diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 3e731cbaffd..291ed5052f4 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -2,6 +2,7 @@ "$schema": "./node_modules/oxfmt/configuration_schema.json", "ignorePatterns": [ ".reference", + ".repos/**", ".plans", "dist", "dist-electron", diff --git a/.oxlintrc.json b/.oxlintrc.json index fe9d133e091..65faed7a5f2 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -3,6 +3,7 @@ "ignorePatterns": [ "dist", "dist-electron", + ".repos/**", "node_modules", "bun.lock", "*.tsbuildinfo", diff --git a/.repos/effect-smol/.agents/skills/grill-me/SKILL.md b/.repos/effect-smol/.agents/skills/grill-me/SKILL.md new file mode 100644 index 00000000000..d65f67b937e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.agents/skills/jsdocs/SKILL.md b/.repos/effect-smol/.agents/skills/jsdocs/SKILL.md new file mode 100644 index 00000000000..50da1b9bdf5 --- /dev/null +++ b/.repos/effect-smol/.agents/skills/jsdocs/SKILL.md @@ -0,0 +1,188 @@ +--- +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...`. +- 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. +- 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-smol/.agents/skills/scratchpad/SKILL.md b/.repos/effect-smol/.agents/skills/scratchpad/SKILL.md new file mode 100644 index 00000000000..b42bdc88fa1 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.agents/skills/scratchpad/agents/openai.yaml b/.repos/effect-smol/.agents/skills/scratchpad/agents/openai.yaml new file mode 100644 index 00000000000..f83a2a4b08a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.agents/skills/scratchpad/scripts/extract-example.mjs b/.repos/effect-smol/.agents/skills/scratchpad/scripts/extract-example.mjs new file mode 100644 index 00000000000..e668122922f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-bigdecimal-sumall-multiplyall.md b/.repos/effect-smol/.changeset/add-bigdecimal-sumall-multiplyall.md new file mode 100644 index 00000000000..3d3a695633d --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-chunk-schema.md b/.repos/effect-smol/.changeset/add-chunk-schema.md new file mode 100644 index 00000000000..890e0fcbfb1 --- /dev/null +++ b/.repos/effect-smol/.changeset/add-chunk-schema.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `Chunk` schema, closes #1585. diff --git a/.repos/effect-smol/.changeset/add-command-hidden.md b/.repos/effect-smol/.changeset/add-command-hidden.md new file mode 100644 index 00000000000..34754c51b40 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-config-nested.md b/.repos/effect-smol/.changeset/add-config-nested.md new file mode 100644 index 00000000000..22f53da810c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-flag-hidden.md b/.repos/effect-smol/.changeset/add-flag-hidden.md new file mode 100644 index 00000000000..1c31e6cd8bf --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-from-string-schemas.md b/.repos/effect-smol/.changeset/add-from-string-schemas.md new file mode 100644 index 00000000000..5ee9f82fbcb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-headers-remove-many.md b/.repos/effect-smol/.changeset/add-headers-remove-many.md new file mode 100644 index 00000000000..8d09bb51674 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-indexeddb-kvs-layer.md b/.repos/effect-smol/.changeset/add-indexeddb-kvs-layer.md new file mode 100644 index 00000000000..bb6671bc7f9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-make-msgpack.md b/.repos/effect-smol/.changeset/add-make-msgpack.md new file mode 100644 index 00000000000..9abec1e9e96 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-make-option.md b/.repos/effect-smol/.changeset/add-make-option.md new file mode 100644 index 00000000000..4984b63192b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-missing-tx-modules.md b/.repos/effect-smol/.changeset/add-missing-tx-modules.md new file mode 100644 index 00000000000..aa7cc305cc1 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-newtype-module.md b/.repos/effect-smol/.changeset/add-newtype-module.md new file mode 100644 index 00000000000..c407aeb54c9 --- /dev/null +++ b/.repos/effect-smol/.changeset/add-newtype-module.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Newtype` module. diff --git a/.repos/effect-smol/.changeset/add-scalar-show-operation-id.md b/.repos/effect-smol/.changeset/add-scalar-show-operation-id.md new file mode 100644 index 00000000000..04d131e1db1 --- /dev/null +++ b/.repos/effect-smol/.changeset/add-scalar-show-operation-id.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `showOperationId` to `HttpApiScalar.ScalarConfig`. diff --git a/.repos/effect-smol/.changeset/add-schedule-tap.md b/.repos/effect-smol/.changeset/add-schedule-tap.md new file mode 100644 index 00000000000..0f4ccc3c5c5 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-schema-annotate-encoded.md b/.repos/effect-smol/.changeset/add-schema-annotate-encoded.md new file mode 100644 index 00000000000..0e5b7e11523 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-schema-array-ensure.md b/.repos/effect-smol/.changeset/add-schema-array-ensure.md new file mode 100644 index 00000000000..908baea8845 --- /dev/null +++ b/.repos/effect-smol/.changeset/add-schema-array-ensure.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Schema.ArrayEnsure`. diff --git a/.repos/effect-smol/.changeset/add-schema-bigdecimal.md b/.repos/effect-smol/.changeset/add-schema-bigdecimal.md new file mode 100644 index 00000000000..76d278fdb9b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-schema-datetime.md b/.repos/effect-smol/.changeset/add-schema-datetime.md new file mode 100644 index 00000000000..c72f4608e3a --- /dev/null +++ b/.repos/effect-smol/.changeset/add-schema-datetime.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `DateTimeZoned`, `TimeZoneOffset`, `TimeZoneNamed`, and `TimeZone` schemas. diff --git a/.repos/effect-smol/.changeset/add-schema-option-from-optional-nullor.md b/.repos/effect-smol/.changeset/add-schema-option-from-optional-nullor.md new file mode 100644 index 00000000000..d634eea40fd --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-schema-option-from-undefined-nullish.md b/.repos/effect-smol/.changeset/add-schema-option-from-undefined-nullish.md new file mode 100644 index 00000000000..c2d569326c8 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-schema-string-encoding.md b/.repos/effect-smol/.changeset/add-schema-string-encoding.md new file mode 100644 index 00000000000..7910ae29f65 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-sql-pglite.md b/.repos/effect-smol/.changeset/add-sql-pglite.md new file mode 100644 index 00000000000..e0eb64eaefb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-standard-jsdoc-rule.md b/.repos/effect-smol/.changeset/add-standard-jsdoc-rule.md new file mode 100644 index 00000000000..317fe356f49 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/add-stream-broadcastn.md b/.repos/effect-smol/.changeset/add-stream-broadcastn.md new file mode 100644 index 00000000000..ca1fa612f27 --- /dev/null +++ b/.repos/effect-smol/.changeset/add-stream-broadcastn.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add Stream.broadcastN for fixed-size stream broadcasts. diff --git a/.repos/effect-smol/.changeset/add-unstable-encoding-export.md b/.repos/effect-smol/.changeset/add-unstable-encoding-export.md new file mode 100644 index 00000000000..99b107249c8 --- /dev/null +++ b/.repos/effect-smol/.changeset/add-unstable-encoding-export.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `unstable/encoding` subpath export. diff --git a/.repos/effect-smol/.changeset/afraid-cobras-like.md b/.repos/effect-smol/.changeset/afraid-cobras-like.md new file mode 100644 index 00000000000..c471ffa3544 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/ai-openai-config-field-leak.md b/.repos/effect-smol/.changeset/ai-openai-config-field-leak.md new file mode 100644 index 00000000000..58df093aa77 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/ai-openai-file-nullable-fields.md b/.repos/effect-smol/.changeset/ai-openai-file-nullable-fields.md new file mode 100644 index 00000000000..b59718e49c3 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/asyncresult-exhaustive.md b/.repos/effect-smol/.changeset/asyncresult-exhaustive.md new file mode 100644 index 00000000000..d448fd604d7 --- /dev/null +++ b/.repos/effect-smol/.changeset/asyncresult-exhaustive.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add an exhaustive finalizer to the AsyncResult builder. diff --git a/.repos/effect-smol/.changeset/atom-stream-error-type.md b/.repos/effect-smol/.changeset/atom-stream-error-type.md new file mode 100644 index 00000000000..a0809cae78e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/beige-paths-sort.md b/.repos/effect-smol/.changeset/beige-paths-sort.md new file mode 100644 index 00000000000..0cfad27ccbc --- /dev/null +++ b/.repos/effect-smol/.changeset/beige-paths-sort.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +move ChildProcess apis into spawner service diff --git a/.repos/effect-smol/.changeset/better-apples-nail.md b/.repos/effect-smol/.changeset/better-apples-nail.md new file mode 100644 index 00000000000..338b25b16eb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/better-rocks-arrive.md b/.repos/effect-smol/.changeset/better-rocks-arrive.md new file mode 100644 index 00000000000..c703f179d4a --- /dev/null +++ b/.repos/effect-smol/.changeset/better-rocks-arrive.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +add back openai reasoning types diff --git a/.repos/effect-smol/.changeset/big-pans-look.md b/.repos/effect-smol/.changeset/big-pans-look.md new file mode 100644 index 00000000000..0e4fd594ff1 --- /dev/null +++ b/.repos/effect-smol/.changeset/big-pans-look.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Rpc.custom diff --git a/.repos/effect-smol/.changeset/blue-dingos-greet.md b/.repos/effect-smol/.changeset/blue-dingos-greet.md new file mode 100644 index 00000000000..01e918bc6cb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/blue-ligers-cheat.md b/.repos/effect-smol/.changeset/blue-ligers-cheat.md new file mode 100644 index 00000000000..37a66d750cb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/blue-onions-smile.md b/.repos/effect-smol/.changeset/blue-onions-smile.md new file mode 100644 index 00000000000..2ef4e625a10 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/blue-ravens-type.md b/.repos/effect-smol/.changeset/blue-ravens-type.md new file mode 100644 index 00000000000..0919ee483cf --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/blue-trams-kiss.md b/.repos/effect-smol/.changeset/blue-trams-kiss.md new file mode 100644 index 00000000000..27fa6f36bb4 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/bold-chairs-yawn.md b/.repos/effect-smol/.changeset/bold-chairs-yawn.md new file mode 100644 index 00000000000..1b14a10a823 --- /dev/null +++ b/.repos/effect-smol/.changeset/bold-chairs-yawn.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add HttpApiTest module diff --git a/.repos/effect-smol/.changeset/breezy-meals-see.md b/.repos/effect-smol/.changeset/breezy-meals-see.md new file mode 100644 index 00000000000..c4a8359c584 --- /dev/null +++ b/.repos/effect-smol/.changeset/breezy-meals-see.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +support tag unions in Effect.catchTag/Reason diff --git a/.repos/effect-smol/.changeset/bright-bugs-bow.md b/.repos/effect-smol/.changeset/bright-bugs-bow.md new file mode 100644 index 00000000000..ca6cb86e969 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/bright-canyons-clean.md b/.repos/effect-smol/.changeset/bright-canyons-clean.md new file mode 100644 index 00000000000..3bddee36c58 --- /dev/null +++ b/.repos/effect-smol/.changeset/bright-canyons-clean.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add support for deferred responses in rpc diff --git a/.repos/effect-smol/.changeset/bright-dogs-fail.md b/.repos/effect-smol/.changeset/bright-dogs-fail.md new file mode 100644 index 00000000000..f29db94184b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/bright-laws-teach.md b/.repos/effect-smol/.changeset/bright-laws-teach.md new file mode 100644 index 00000000000..12df791c78f --- /dev/null +++ b/.repos/effect-smol/.changeset/bright-laws-teach.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Config.Success` type utility, closes #1783. diff --git a/.repos/effect-smol/.changeset/bright-lemons-dance.md b/.repos/effect-smol/.changeset/bright-lemons-dance.md new file mode 100644 index 00000000000..0bae9332d91 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/bright-planes-smash.md b/.repos/effect-smol/.changeset/bright-planes-smash.md new file mode 100644 index 00000000000..edf82b8b259 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/bright-rats-attend.md b/.repos/effect-smol/.changeset/bright-rats-attend.md new file mode 100644 index 00000000000..071f37890d6 --- /dev/null +++ b/.repos/effect-smol/.changeset/bright-rats-attend.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +make fiber keepAlive setInterval evaluation lazy diff --git a/.repos/effect-smol/.changeset/bright-toes-rush.md b/.repos/effect-smol/.changeset/bright-toes-rush.md new file mode 100644 index 00000000000..34bda7c4358 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/bumpy-boxes-teach.md b/.repos/effect-smol/.changeset/bumpy-boxes-teach.md new file mode 100644 index 00000000000..31205424ef3 --- /dev/null +++ b/.repos/effect-smol/.changeset/bumpy-boxes-teach.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename Atom's `Context` type to `AtomContext` diff --git a/.repos/effect-smol/.changeset/busy-lions-sneeze.md b/.repos/effect-smol/.changeset/busy-lions-sneeze.md new file mode 100644 index 00000000000..de2167c4a85 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/busy-maps-attend.md b/.repos/effect-smol/.changeset/busy-maps-attend.md new file mode 100644 index 00000000000..395698754a3 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/calm-buckets-own.md b/.repos/effect-smol/.changeset/calm-buckets-own.md new file mode 100644 index 00000000000..0610fbdbabc --- /dev/null +++ b/.repos/effect-smol/.changeset/calm-buckets-own.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +improve openai socket errors diff --git a/.repos/effect-smol/.changeset/calm-carrots-march.md b/.repos/effect-smol/.changeset/calm-carrots-march.md new file mode 100644 index 00000000000..75703506f1e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/calm-cars-rest.md b/.repos/effect-smol/.changeset/calm-cars-rest.md new file mode 100644 index 00000000000..a61b82e7714 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/calm-panthers-nail.md b/.repos/effect-smol/.changeset/calm-panthers-nail.md new file mode 100644 index 00000000000..1f855e558ea --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/calm-squids-hug.md b/.repos/effect-smol/.changeset/calm-squids-hug.md new file mode 100644 index 00000000000..3ac3c61ab97 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/chatty-poets-type.md b/.repos/effect-smol/.changeset/chatty-poets-type.md new file mode 100644 index 00000000000..623273cfbfe --- /dev/null +++ b/.repos/effect-smol/.changeset/chatty-poets-type.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +remove Effect.Yieldable diff --git a/.repos/effect-smol/.changeset/chilled-mice-wash.md b/.repos/effect-smol/.changeset/chilled-mice-wash.md new file mode 100644 index 00000000000..08e529a7e91 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/chilly-pumas-rule.md b/.repos/effect-smol/.changeset/chilly-pumas-rule.md new file mode 100644 index 00000000000..b53d3514c59 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/chubby-buckets-feel.md b/.repos/effect-smol/.changeset/chubby-buckets-feel.md new file mode 100644 index 00000000000..bf1cc12f949 --- /dev/null +++ b/.repos/effect-smol/.changeset/chubby-buckets-feel.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix issues with metro bundler diff --git a/.repos/effect-smol/.changeset/chubby-parents-flow.md b/.repos/effect-smol/.changeset/chubby-parents-flow.md new file mode 100644 index 00000000000..8c87cad403f --- /dev/null +++ b/.repos/effect-smol/.changeset/chubby-parents-flow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow undefined for VariantSchema.Overridable input diff --git a/.repos/effect-smol/.changeset/chubby-planets-fall.md b/.repos/effect-smol/.changeset/chubby-planets-fall.md new file mode 100644 index 00000000000..15c60d59256 --- /dev/null +++ b/.repos/effect-smol/.changeset/chubby-planets-fall.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Remove duplicate `ToolApprovalResponsePartOptions` from Anthropic package diff --git a/.repos/effect-smol/.changeset/clean-balloons-tan.md b/.repos/effect-smol/.changeset/clean-balloons-tan.md new file mode 100644 index 00000000000..1c4ff471825 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/clean-bulldogs-care.md b/.repos/effect-smol/.changeset/clean-bulldogs-care.md new file mode 100644 index 00000000000..48f430f9f6c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/clean-dryers-sneeze.md b/.repos/effect-smol/.changeset/clean-dryers-sneeze.md new file mode 100644 index 00000000000..96316e62f8c --- /dev/null +++ b/.repos/effect-smol/.changeset/clean-dryers-sneeze.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Random.nextBoolean` for generating random boolean values. diff --git a/.repos/effect-smol/.changeset/clean-geese-work.md b/.repos/effect-smol/.changeset/clean-geese-work.md new file mode 100644 index 00000000000..e91346b785e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/clean-goats-wave.md b/.repos/effect-smol/.changeset/clean-goats-wave.md new file mode 100644 index 00000000000..944fd84305f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/clean-tires-guess.md b/.repos/effect-smol/.changeset/clean-tires-guess.md new file mode 100644 index 00000000000..29463e91de3 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/clear-spies-boil.md b/.repos/effect-smol/.changeset/clear-spies-boil.md new file mode 100644 index 00000000000..44318f6f5df --- /dev/null +++ b/.repos/effect-smol/.changeset/clear-spies-boil.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +improve idb support for compound indexes diff --git a/.repos/effect-smol/.changeset/cli-help-choices.md b/.repos/effect-smol/.changeset/cli-help-choices.md new file mode 100644 index 00000000000..379f75198bb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/cold-knives-lie.md b/.repos/effect-smol/.changeset/cold-knives-lie.md new file mode 100644 index 00000000000..5e7051fd359 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/cold-rooms-show.md b/.repos/effect-smol/.changeset/cold-rooms-show.md new file mode 100644 index 00000000000..ab2258a8121 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/cold-sloths-wave.md b/.repos/effect-smol/.changeset/cold-sloths-wave.md new file mode 100644 index 00000000000..6e6a1f7fc9a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/compact-json-schema-enum.md b/.repos/effect-smol/.changeset/compact-json-schema-enum.md new file mode 100644 index 00000000000..f6b4e7633e9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/config-withdefault-eager.md b/.repos/effect-smol/.changeset/config-withdefault-eager.md new file mode 100644 index 00000000000..107cd6fd7b2 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/config.json b/.repos/effect-smol/.changeset/config.json new file mode 100644 index 00000000000..b45ea0c70c6 --- /dev/null +++ b/.repos/effect-smol/.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-smol" }], + "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-smol/.changeset/consolidate-encoding.md b/.repos/effect-smol/.changeset/consolidate-encoding.md new file mode 100644 index 00000000000..8e0cf7c052b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/consolidate-sql-error.md b/.repos/effect-smol/.changeset/consolidate-sql-error.md new file mode 100644 index 00000000000..fdefa5f8cb6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/crisp-seas-warn.md b/.repos/effect-smol/.changeset/crisp-seas-warn.md new file mode 100644 index 00000000000..31fac078f64 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/cuddly-rooms-bet.md b/.repos/effect-smol/.changeset/cuddly-rooms-bet.md new file mode 100644 index 00000000000..8d09b0ad84f --- /dev/null +++ b/.repos/effect-smol/.changeset/cuddly-rooms-bet.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Stream.timeoutOrElse diff --git a/.repos/effect-smol/.changeset/curly-poems-talk.md b/.repos/effect-smol/.changeset/curly-poems-talk.md new file mode 100644 index 00000000000..97fa70b92b3 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/curly-spies-relax.md b/.repos/effect-smol/.changeset/curly-spies-relax.md new file mode 100644 index 00000000000..a495ef8bc9c --- /dev/null +++ b/.repos/effect-smol/.changeset/curly-spies-relax.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +narrow types for Effect.retry/repeat while option diff --git a/.repos/effect-smol/.changeset/curvy-apples-float.md b/.repos/effect-smol/.changeset/curvy-apples-float.md new file mode 100644 index 00000000000..fcdfac63422 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/custom-http-security-openapi-generator.md b/.repos/effect-smol/.changeset/custom-http-security-openapi-generator.md new file mode 100644 index 00000000000..c338435df38 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/cute-heads-thank.md b/.repos/effect-smol/.changeset/cute-heads-thank.md new file mode 100644 index 00000000000..25a2f17e3f3 --- /dev/null +++ b/.repos/effect-smol/.changeset/cute-heads-thank.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +catch defects when building Entity handlers diff --git a/.repos/effect-smol/.changeset/cyan-loops-grow.md b/.repos/effect-smol/.changeset/cyan-loops-grow.md new file mode 100644 index 00000000000..a807ca082a5 --- /dev/null +++ b/.repos/effect-smol/.changeset/cyan-loops-grow.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +improve idb transaction api diff --git a/.repos/effect-smol/.changeset/cyan-radios-switch.md b/.repos/effect-smol/.changeset/cyan-radios-switch.md new file mode 100644 index 00000000000..6763e295364 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/deep-rivers-spend.md b/.repos/effect-smol/.changeset/deep-rivers-spend.md new file mode 100644 index 00000000000..2c89a0793b7 --- /dev/null +++ b/.repos/effect-smol/.changeset/deep-rivers-spend.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix Stream.withSpan options diff --git a/.repos/effect-smol/.changeset/dirty-lamps-trade.md b/.repos/effect-smol/.changeset/dirty-lamps-trade.md new file mode 100644 index 00000000000..26c515d5152 --- /dev/null +++ b/.repos/effect-smol/.changeset/dirty-lamps-trade.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Result.failVoid diff --git a/.repos/effect-smol/.changeset/dirty-laws-wear.md b/.repos/effect-smol/.changeset/dirty-laws-wear.md new file mode 100644 index 00000000000..68ad0d5a8e4 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/duration-temporal-object-input.md b/.repos/effect-smol/.changeset/duration-temporal-object-input.md new file mode 100644 index 00000000000..faa02f6718f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eager-coats-cheat.md b/.repos/effect-smol/.changeset/eager-coats-cheat.md new file mode 100644 index 00000000000..d75b97ae8c1 --- /dev/null +++ b/.repos/effect-smol/.changeset/eager-coats-cheat.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +rename SqlSchema.findOne\* apis diff --git a/.repos/effect-smol/.changeset/early-birds-dream.md b/.repos/effect-smol/.changeset/early-birds-dream.md new file mode 100644 index 00000000000..a6de037331a --- /dev/null +++ b/.repos/effect-smol/.changeset/early-birds-dream.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add trait for customizing exit codes diff --git a/.repos/effect-smol/.changeset/early-donuts-argue.md b/.repos/effect-smol/.changeset/early-donuts-argue.md new file mode 100644 index 00000000000..fe1b7017711 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/early-peaches-check.md b/.repos/effect-smol/.changeset/early-peaches-check.md new file mode 100644 index 00000000000..0782936ad42 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-691-default-logger-ordering.md b/.repos/effect-smol/.changeset/eff-691-default-logger-ordering.md new file mode 100644 index 00000000000..91fe0547d01 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-693-rpcgroup-handler-deps.md b/.repos/effect-smol/.changeset/eff-693-rpcgroup-handler-deps.md new file mode 100644 index 00000000000..e812b194a9d --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-694-cli-completions-module.md b/.repos/effect-smol/.changeset/eff-694-cli-completions-module.md new file mode 100644 index 00000000000..fcce2923db7 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-695-layer-mock-dual-api.md b/.repos/effect-smol/.changeset/eff-695-layer-mock-dual-api.md new file mode 100644 index 00000000000..b4766bb3faf --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-697-rpcserialization-json-array-decode.md b/.repos/effect-smol/.changeset/eff-697-rpcserialization-json-array-decode.md new file mode 100644 index 00000000000..062fcf8637d --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-698-rpcserialization-unreachable-branch.md b/.repos/effect-smol/.changeset/eff-698-rpcserialization-unreachable-branch.md new file mode 100644 index 00000000000..cb1b1de9965 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-700-httpapi-middleware-errors.md b/.repos/effect-smol/.changeset/eff-700-httpapi-middleware-errors.md new file mode 100644 index 00000000000..e95a36007cb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-701-httpapierror-respondable.md b/.repos/effect-smol/.changeset/eff-701-httpapierror-respondable.md new file mode 100644 index 00000000000..9b78e505ee3 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-704-stream-merge-predicate.md b/.repos/effect-smol/.changeset/eff-704-stream-merge-predicate.md new file mode 100644 index 00000000000..a3122608917 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-705-layer-tap-apis.md b/.repos/effect-smol/.changeset/eff-705-layer-tap-apis.md new file mode 100644 index 00000000000..c6b96bc0dd2 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-706-servicemap-mutate.md b/.repos/effect-smol/.changeset/eff-706-servicemap-mutate.md new file mode 100644 index 00000000000..6ce63c443a9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-716-response-id-tracker-map.md b/.repos/effect-smol/.changeset/eff-716-response-id-tracker-map.md new file mode 100644 index 00000000000..8605f1bc897 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-717-openai-socket-cancel.md b/.repos/effect-smol/.changeset/eff-717-openai-socket-cancel.md new file mode 100644 index 00000000000..d4adf58a724 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-718-embedding-model-surface.md b/.repos/effect-smol/.changeset/eff-718-embedding-model-surface.md new file mode 100644 index 00000000000..e8754bd3e9b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-725-fix-catch-jsdoc.md b/.repos/effect-smol/.changeset/eff-725-fix-catch-jsdoc.md new file mode 100644 index 00000000000..a5ca6b5222e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-726-model-dimensions.md b/.repos/effect-smol/.changeset/eff-726-model-dimensions.md new file mode 100644 index 00000000000..1a6022bcffc --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-727-cli-help-alignment.md b/.repos/effect-smol/.changeset/eff-727-cli-help-alignment.md new file mode 100644 index 00000000000..32e9ada50d5 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-730-language-model-incremental-fallback.md b/.repos/effect-smol/.changeset/eff-730-language-model-incremental-fallback.md new file mode 100644 index 00000000000..55fa6a0a2d6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-736-cached-with-ttl.md b/.repos/effect-smol/.changeset/eff-736-cached-with-ttl.md new file mode 100644 index 00000000000..8ef05290bf9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-738-cron-prev.md b/.repos/effect-smol/.changeset/eff-738-cron-prev.md new file mode 100644 index 00000000000..b9109d4b713 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-739-openai-function-call-done.md b/.repos/effect-smol/.changeset/eff-739-openai-function-call-done.md new file mode 100644 index 00000000000..90693137778 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-740-missing-summary-parts.md b/.repos/effect-smol/.changeset/eff-740-missing-summary-parts.md new file mode 100644 index 00000000000..c481d830ed4 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-742-http-client-request-web.md b/.repos/effect-smol/.changeset/eff-742-http-client-request-web.md new file mode 100644 index 00000000000..47c201686e4 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-744-sqlite-migrator-lock.md b/.repos/effect-smol/.changeset/eff-744-sqlite-migrator-lock.md new file mode 100644 index 00000000000..badee2a510b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-746-fixed-iteration-catchup.md b/.repos/effect-smol/.changeset/eff-746-fixed-iteration-catchup.md new file mode 100644 index 00000000000..db267d594ca --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-747-unify-effect.md b/.repos/effect-smol/.changeset/eff-747-unify-effect.md new file mode 100644 index 00000000000..251737c054b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-754-url-builder-any.md b/.repos/effect-smol/.changeset/eff-754-url-builder-any.md new file mode 100644 index 00000000000..9dbd7bbc301 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-755-references-core.md b/.repos/effect-smol/.changeset/eff-755-references-core.md new file mode 100644 index 00000000000..76715f96ab7 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-769-select-text-highlight.md b/.repos/effect-smol/.changeset/eff-769-select-text-highlight.md new file mode 100644 index 00000000000..f69988f18cd --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-774-mutable-list-append-all-empty-array.md b/.repos/effect-smol/.changeset/eff-774-mutable-list-append-all-empty-array.md new file mode 100644 index 00000000000..7a3ee8cf097 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-777-schema-make-effect.md b/.repos/effect-smol/.changeset/eff-777-schema-make-effect.md new file mode 100644 index 00000000000..d80d6151e70 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-778-http-middleware-path-logger.md b/.repos/effect-smol/.changeset/eff-778-http-middleware-path-logger.md new file mode 100644 index 00000000000..716bc473344 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-779-keyvaluestore-layer-sql.md b/.repos/effect-smol/.changeset/eff-779-keyvaluestore-layer-sql.md new file mode 100644 index 00000000000..ce80278ff01 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-780-layer-unify.md b/.repos/effect-smol/.changeset/eff-780-layer-unify.md new file mode 100644 index 00000000000..59bc0c2a45a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-781-fix-stream-toqueue-types.md b/.repos/effect-smol/.changeset/eff-781-fix-stream-toqueue-types.md new file mode 100644 index 00000000000..3f3ff719f81 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-782-httpapi-status-literals.md b/.repos/effect-smol/.changeset/eff-782-httpapi-status-literals.md new file mode 100644 index 00000000000..6eb4c1ec9fe --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-783-atom-http-api-errors.md b/.repos/effect-smol/.changeset/eff-783-atom-http-api-errors.md new file mode 100644 index 00000000000..f62f2ae8f15 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eff-819-cluster-workflow-shard-groups.md b/.repos/effect-smol/.changeset/eff-819-cluster-workflow-shard-groups.md new file mode 100644 index 00000000000..852c5833987 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eight-turkeys-own.md b/.repos/effect-smol/.changeset/eight-turkeys-own.md new file mode 100644 index 00000000000..1665cd0a44a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eighty-lies-deny.md b/.repos/effect-smol/.changeset/eighty-lies-deny.md new file mode 100644 index 00000000000..3b6e333ce9c --- /dev/null +++ b/.repos/effect-smol/.changeset/eighty-lies-deny.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +change default ErrorReporter severity to Info diff --git a/.repos/effect-smol/.changeset/eighty-poets-draw.md b/.repos/effect-smol/.changeset/eighty-poets-draw.md new file mode 100644 index 00000000000..27a23ed375a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eighty-swans-scream.md b/.repos/effect-smol/.changeset/eighty-swans-scream.md new file mode 100644 index 00000000000..eff50920374 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eighty-teeth-sniff.md b/.repos/effect-smol/.changeset/eighty-teeth-sniff.md new file mode 100644 index 00000000000..4ce98bc8c99 --- /dev/null +++ b/.repos/effect-smol/.changeset/eighty-teeth-sniff.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +simplify Filter by removing Args type parameter diff --git a/.repos/effect-smol/.changeset/eleven-apes-share.md b/.repos/effect-smol/.changeset/eleven-apes-share.md new file mode 100644 index 00000000000..4e63ebc137a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eleven-numbers-bake.md b/.repos/effect-smol/.changeset/eleven-numbers-bake.md new file mode 100644 index 00000000000..7966ef89b9b --- /dev/null +++ b/.repos/effect-smol/.changeset/eleven-numbers-bake.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +pass workflow parent on discard diff --git a/.repos/effect-smol/.changeset/empty-gifts-beg.md b/.repos/effect-smol/.changeset/empty-gifts-beg.md new file mode 100644 index 00000000000..c7d39823c2c --- /dev/null +++ b/.repos/effect-smol/.changeset/empty-gifts-beg.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +process schema properties / elements concurrently diff --git a/.repos/effect-smol/.changeset/empty-http-rpc-client.md b/.repos/effect-smol/.changeset/empty-http-rpc-client.md new file mode 100644 index 00000000000..36685f7eec2 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/eventlog-unencrypted.md b/.repos/effect-smol/.changeset/eventlog-unencrypted.md new file mode 100644 index 00000000000..2a3e8a8e88d --- /dev/null +++ b/.repos/effect-smol/.changeset/eventlog-unencrypted.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add EventLogServerUnencrypted module diff --git a/.repos/effect-smol/.changeset/expand-schema-filter-output.md b/.repos/effect-smol/.changeset/expand-schema-filter-output.md new file mode 100644 index 00000000000..c269804bd0e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/export-schema-encode-keys-interface.md b/.repos/effect-smol/.changeset/export-schema-encode-keys-interface.md new file mode 100644 index 00000000000..2290cd6884a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/extract-semaphore-latch.md b/.repos/effect-smol/.changeset/extract-semaphore-latch.md new file mode 100644 index 00000000000..755512bcc40 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fair-bees-relax.md b/.repos/effect-smol/.changeset/fair-bees-relax.md new file mode 100644 index 00000000000..4e711ae3eee --- /dev/null +++ b/.repos/effect-smol/.changeset/fair-bees-relax.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Fix missing `yield*` in `OpenAiLanguageModel.prepareResponseFormat` diff --git a/.repos/effect-smol/.changeset/fair-buttons-share.md b/.repos/effect-smol/.changeset/fair-buttons-share.md new file mode 100644 index 00000000000..397f1293ef9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fair-cooks-stop.md b/.repos/effect-smol/.changeset/fair-cooks-stop.md new file mode 100644 index 00000000000..9aabf7f8fea --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fair-cups-train.md b/.repos/effect-smol/.changeset/fair-cups-train.md new file mode 100644 index 00000000000..d72aa2f9f88 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fair-dryers-speak.md b/.repos/effect-smol/.changeset/fair-dryers-speak.md new file mode 100644 index 00000000000..cef623e1fcc --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fair-forks-shake.md b/.repos/effect-smol/.changeset/fair-forks-shake.md new file mode 100644 index 00000000000..395d1ec4344 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fair-pandas-prove.md b/.repos/effect-smol/.changeset/fair-pandas-prove.md new file mode 100644 index 00000000000..d88f5fbc4fe --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fair-pants-float.md b/.repos/effect-smol/.changeset/fair-pants-float.md new file mode 100644 index 00000000000..020c76aff91 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fair-poems-visit.md b/.repos/effect-smol/.changeset/fair-poems-visit.md new file mode 100644 index 00000000000..3aa7a731d1b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/famous-wolves-lead.md b/.repos/effect-smol/.changeset/famous-wolves-lead.md new file mode 100644 index 00000000000..19f2bbad4e4 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fancy-glasses-grow.md b/.repos/effect-smol/.changeset/fancy-glasses-grow.md new file mode 100644 index 00000000000..1f55db96aaf --- /dev/null +++ b/.repos/effect-smol/.changeset/fancy-glasses-grow.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +add IndexedDb modules diff --git a/.repos/effect-smol/.changeset/fast-times-camp.md b/.repos/effect-smol/.changeset/fast-times-camp.md new file mode 100644 index 00000000000..f44c6cb8b16 --- /dev/null +++ b/.repos/effect-smol/.changeset/fast-times-camp.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allocate less effects when reading a file diff --git a/.repos/effect-smol/.changeset/few-birds-matter.md b/.repos/effect-smol/.changeset/few-birds-matter.md new file mode 100644 index 00000000000..66fdca809b6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/few-cougars-dig.md b/.repos/effect-smol/.changeset/few-cougars-dig.md new file mode 100644 index 00000000000..86055e698b1 --- /dev/null +++ b/.repos/effect-smol/.changeset/few-cougars-dig.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Effect.abortSignal diff --git a/.repos/effect-smol/.changeset/few-foxes-grin.md b/.repos/effect-smol/.changeset/few-foxes-grin.md new file mode 100644 index 00000000000..8d7631f5c64 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/few-mirrors-pull.md b/.repos/effect-smol/.changeset/few-mirrors-pull.md new file mode 100644 index 00000000000..dd1bee2979e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/few-socks-poke.md b/.repos/effect-smol/.changeset/few-socks-poke.md new file mode 100644 index 00000000000..cdc249ea7fb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fiber-runtime-start-metrics.md b/.repos/effect-smol/.changeset/fiber-runtime-start-metrics.md new file mode 100644 index 00000000000..212fc649365 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fiery-jokes-care.md b/.repos/effect-smol/.changeset/fiery-jokes-care.md new file mode 100644 index 00000000000..0093f23187a --- /dev/null +++ b/.repos/effect-smol/.changeset/fiery-jokes-care.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix Entity.keepAlive diff --git a/.repos/effect-smol/.changeset/fiery-mammals-call.md b/.repos/effect-smol/.changeset/fiery-mammals-call.md new file mode 100644 index 00000000000..2fce3b1f482 --- /dev/null +++ b/.repos/effect-smol/.changeset/fiery-mammals-call.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure tagged enum \_tag is correctly set diff --git a/.repos/effect-smol/.changeset/fine-walls-decide.md b/.repos/effect-smol/.changeset/fine-walls-decide.md new file mode 100644 index 00000000000..7f6e67dfc49 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/first-success-of.md b/.repos/effect-smol/.changeset/first-success-of.md new file mode 100644 index 00000000000..e31e6792a7a --- /dev/null +++ b/.repos/effect-smol/.changeset/first-success-of.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Port `Effect.firstSuccessOf` from Effect v3. diff --git a/.repos/effect-smol/.changeset/five-parents-relax.md b/.repos/effect-smol/.changeset/five-parents-relax.md new file mode 100644 index 00000000000..15192e8dc93 --- /dev/null +++ b/.repos/effect-smol/.changeset/five-parents-relax.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +allow customizing idb durability diff --git a/.repos/effect-smol/.changeset/five-worms-rhyme.md b/.repos/effect-smol/.changeset/five-worms-rhyme.md new file mode 100644 index 00000000000..01707d165db --- /dev/null +++ b/.repos/effect-smol/.changeset/five-worms-rhyme.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix eventlog skipping entries diff --git a/.repos/effect-smol/.changeset/fix-1332.md b/.repos/effect-smol/.changeset/fix-1332.md new file mode 100644 index 00000000000..4ab4fa12376 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-1917.md b/.repos/effect-smol/.changeset/fix-1917.md new file mode 100644 index 00000000000..f7cf342ee16 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-1927.md b/.repos/effect-smol/.changeset/fix-1927.md new file mode 100644 index 00000000000..7882a2191c1 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-1940.md b/.repos/effect-smol/.changeset/fix-1940.md new file mode 100644 index 00000000000..be9ef1a1f53 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-1947.md b/.repos/effect-smol/.changeset/fix-1947.md new file mode 100644 index 00000000000..f5cd72ff760 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-2002.md b/.repos/effect-smol/.changeset/fix-2002.md new file mode 100644 index 00000000000..a2de2031812 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-2012.md b/.repos/effect-smol/.changeset/fix-2012.md new file mode 100644 index 00000000000..6b7ae930af0 --- /dev/null +++ b/.repos/effect-smol/.changeset/fix-2012.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add withDecodingDefaultTypeKey / withDecodingDefaultType, closes #2012 diff --git a/.repos/effect-smol/.changeset/fix-2015.md b/.repos/effect-smol/.changeset/fix-2015.md new file mode 100644 index 00000000000..067530685a6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-2260.md b/.repos/effect-smol/.changeset/fix-2260.md new file mode 100644 index 00000000000..dddb2c1e492 --- /dev/null +++ b/.repos/effect-smol/.changeset/fix-2260.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add JSON Schema custom annotation passthrough option, closes #2260 diff --git a/.repos/effect-smol/.changeset/fix-2268.md b/.repos/effect-smol/.changeset/fix-2268.md new file mode 100644 index 00000000000..b0977d4a1eb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-2271.md b/.repos/effect-smol/.changeset/fix-2271.md new file mode 100644 index 00000000000..1be94c36b70 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-ai-empty-params-structured-output.md b/.repos/effect-smol/.changeset/fix-ai-empty-params-structured-output.md new file mode 100644 index 00000000000..aa3806870bd --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-ai-text-toolkit-typing.md b/.repos/effect-smol/.changeset/fix-ai-text-toolkit-typing.md new file mode 100644 index 00000000000..da37eea4c2c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-catch-orelse-error-erasure.md b/.repos/effect-smol/.changeset/fix-catch-orelse-error-erasure.md new file mode 100644 index 00000000000..0f4846bf3a9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-class-constructor-defaults.md b/.repos/effect-smol/.changeset/fix-class-constructor-defaults.md new file mode 100644 index 00000000000..8e9e824b1d2 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-cli-mixed-global-flag-context.md b/.repos/effect-smol/.changeset/fix-cli-mixed-global-flag-context.md new file mode 100644 index 00000000000..73ab0d1e402 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-config-withDefault.md b/.repos/effect-smol/.changeset/fix-config-withDefault.md new file mode 100644 index 00000000000..a0ce253d75c --- /dev/null +++ b/.repos/effect-smol/.changeset/fix-config-withDefault.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Config.withDefault` type inference, closes #1530. diff --git a/.repos/effect-smol/.changeset/fix-datetime-gmt.md b/.repos/effect-smol/.changeset/fix-datetime-gmt.md new file mode 100644 index 00000000000..02baaa85d85 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-devtools-flush-on-teardown.md b/.repos/effect-smol/.changeset/fix-devtools-flush-on-teardown.md new file mode 100644 index 00000000000..d5c46f9e12b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-entity-proxy-rpc-handler-context.md b/.repos/effect-smol/.changeset/fix-entity-proxy-rpc-handler-context.md new file mode 100644 index 00000000000..9b4e1c3c1be --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-entity-proxy-server-path-params.md b/.repos/effect-smol/.changeset/fix-entity-proxy-server-path-params.md new file mode 100644 index 00000000000..2cbf13f11dc --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-has-interrupts-only-empty.md b/.repos/effect-smol/.changeset/fix-has-interrupts-only-empty.md new file mode 100644 index 00000000000..5434d7a384f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-hashmap-bit31-ordering.md b/.repos/effect-smol/.changeset/fix-hashmap-bit31-ordering.md new file mode 100644 index 00000000000..b56bac4739e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-headers-proto-enumerability.md b/.repos/effect-smol/.changeset/fix-headers-proto-enumerability.md new file mode 100644 index 00000000000..686c038ff24 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-http-incoming-message-parse-options.md b/.repos/effect-smol/.changeset/fix-http-incoming-message-parse-options.md new file mode 100644 index 00000000000..1f71f78c82c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-httpapi-security-middleware-cache.md b/.repos/effect-smol/.changeset/fix-httpapi-security-middleware-cache.md new file mode 100644 index 00000000000..cf9e68fbb79 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-is-json-dag.md b/.repos/effect-smol/.changeset/fix-is-json-dag.md new file mode 100644 index 00000000000..68809e071f6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-json-schema-anyof-oneof-siblings.md b/.repos/effect-smol/.changeset/fix-json-schema-anyof-oneof-siblings.md new file mode 100644 index 00000000000..9f0840d1e35 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-keepalive-blocked-timers.md b/.repos/effect-smol/.changeset/fix-keepalive-blocked-timers.md new file mode 100644 index 00000000000..8cdb6fa16ca --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-mcp-param-name-resolution.md b/.repos/effect-smol/.changeset/fix-mcp-param-name-resolution.md new file mode 100644 index 00000000000..2ec5b38416c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-mermaid-escape-special-chars.md b/.repos/effect-smol/.changeset/fix-mermaid-escape-special-chars.md new file mode 100644 index 00000000000..873f71b9209 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-mutable-list-filter-length.md b/.repos/effect-smol/.changeset/fix-mutable-list-filter-length.md new file mode 100644 index 00000000000..c6a6a84fdf0 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-openai-mcp-tool-names.md b/.repos/effect-smol/.changeset/fix-openai-mcp-tool-names.md new file mode 100644 index 00000000000..c6009ba2b82 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-openapi-generator-form-urlencoded.md b/.repos/effect-smol/.changeset/fix-openapi-generator-form-urlencoded.md new file mode 100644 index 00000000000..0b37c34bdd9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-openapi-preserve-multiple-response-content-types.md b/.repos/effect-smol/.changeset/fix-openapi-preserve-multiple-response-content-types.md new file mode 100644 index 00000000000..7074326ffef --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-openrouter-sparse-array.md b/.repos/effect-smol/.changeset/fix-openrouter-sparse-array.md new file mode 100644 index 00000000000..74f87f92f20 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-otel-logger-clock-skew.md b/.repos/effect-smol/.changeset/fix-otel-logger-clock-skew.md new file mode 100644 index 00000000000..8e6ae29679e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-otel-logger-severity-number.md b/.repos/effect-smol/.changeset/fix-otel-logger-severity-number.md new file mode 100644 index 00000000000..b411ad4d70a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-queue-collect-duplication.md b/.repos/effect-smol/.changeset/fix-queue-collect-duplication.md new file mode 100644 index 00000000000..bc29d2ee2d1 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-remainder-scientific-notation.md b/.repos/effect-smol/.changeset/fix-remainder-scientific-notation.md new file mode 100644 index 00000000000..1a0b109d97f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-request-resolver-pending-batches-leak.md b/.repos/effect-smol/.changeset/fix-request-resolver-pending-batches-leak.md new file mode 100644 index 00000000000..9721cd17552 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-retry-transient-autocomplete.md b/.repos/effect-smol/.changeset/fix-retry-transient-autocomplete.md new file mode 100644 index 00000000000..0cdb6b2af3a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-rpc-http-requestids-finalizer.md b/.repos/effect-smol/.changeset/fix-rpc-http-requestids-finalizer.md new file mode 100644 index 00000000000..8c295f89952 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-rpc-json-id-edges.md b/.repos/effect-smol/.changeset/fix-rpc-json-id-edges.md new file mode 100644 index 00000000000..6b915e0d03c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-schedule-fixed-double-exec.md b/.repos/effect-smol/.changeset/fix-schedule-fixed-double-exec.md new file mode 100644 index 00000000000..1909b59a16d --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-schedule-reduce-sync-state.md b/.repos/effect-smol/.changeset/fix-schedule-reduce-sync-state.md new file mode 100644 index 00000000000..9c1d9293514 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-schema-arbitrary-exclusive-bounds.md b/.repos/effect-smol/.changeset/fix-schema-arbitrary-exclusive-bounds.md new file mode 100644 index 00000000000..b14291c8da4 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-schema-encodekeys-class.md b/.repos/effect-smol/.changeset/fix-schema-encodekeys-class.md new file mode 100644 index 00000000000..ebf61c22efb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-schema-encodekeys-struct.md b/.repos/effect-smol/.changeset/fix-schema-encodekeys-struct.md new file mode 100644 index 00000000000..fd884d1efd1 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-schema-identifier-expected-message.md b/.repos/effect-smol/.changeset/fix-schema-identifier-expected-message.md new file mode 100644 index 00000000000..8db64967704 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-schema-is-uuid.md b/.repos/effect-smol/.changeset/fix-schema-is-uuid.md new file mode 100644 index 00000000000..ecd2eb12f2a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-searchparam-initial-decode.md b/.repos/effect-smol/.changeset/fix-searchparam-initial-decode.md new file mode 100644 index 00000000000..f78d96a4636 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-serializable-wire-transfer.md b/.repos/effect-smol/.changeset/fix-serializable-wire-transfer.md new file mode 100644 index 00000000000..64c580d8780 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-stream-grouped-within-flush.md b/.repos/effect-smol/.changeset/fix-stream-grouped-within-flush.md new file mode 100644 index 00000000000..fff052fc77e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-stream-scan-effect.md b/.repos/effect-smol/.changeset/fix-stream-scan-effect.md new file mode 100644 index 00000000000..a1d821f93c6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-stream-scoped-scope.md b/.repos/effect-smol/.changeset/fix-stream-scoped-scope.md new file mode 100644 index 00000000000..c4437e65616 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-strip-approval-artifacts-multi-round.md b/.repos/effect-smol/.changeset/fix-strip-approval-artifacts-multi-round.md new file mode 100644 index 00000000000..c091d0313d2 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-struct-utility-types-simplify.md b/.repos/effect-smol/.changeset/fix-struct-utility-types-simplify.md new file mode 100644 index 00000000000..46077032768 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-tagged-union-class-sentinels.md b/.repos/effect-smol/.changeset/fix-tagged-union-class-sentinels.md new file mode 100644 index 00000000000..f79912a9714 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-tagged-union-match-unify.md b/.repos/effect-smol/.changeset/fix-tagged-union-match-unify.md new file mode 100644 index 00000000000..987abe5e624 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-tuple-with-rest-post-rest-index-drift.md b/.repos/effect-smol/.changeset/fix-tuple-with-rest-post-rest-index-drift.md new file mode 100644 index 00000000000..155671bec3f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-tuple-with-rest-post-rest-validation.md b/.repos/effect-smol/.changeset/fix-tuple-with-rest-post-rest-validation.md new file mode 100644 index 00000000000..60e4272f533 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-types-voidifempty.md b/.repos/effect-smol/.changeset/fix-types-voidifempty.md new file mode 100644 index 00000000000..f481e77957e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-void-response-encoding.md b/.repos/effect-smol/.changeset/fix-void-response-encoding.md new file mode 100644 index 00000000000..bb84b57ff3f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fix-workflow-proxy-rpc-handler-context.md b/.repos/effect-smol/.changeset/fix-workflow-proxy-rpc-handler-context.md new file mode 100644 index 00000000000..cc1541b66bf --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/flat-chicken-remain.md b/.repos/effect-smol/.changeset/flat-chicken-remain.md new file mode 100644 index 00000000000..17b0598595d --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/floppy-cows-spend.md b/.repos/effect-smol/.changeset/floppy-cows-spend.md new file mode 100644 index 00000000000..895423806ab --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/floppy-items-admire.md b/.repos/effect-smol/.changeset/floppy-items-admire.md new file mode 100644 index 00000000000..40c31321301 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/floppy-pigs-kiss.md b/.repos/effect-smol/.changeset/floppy-pigs-kiss.md new file mode 100644 index 00000000000..0dfba7bb7e5 --- /dev/null +++ b/.repos/effect-smol/.changeset/floppy-pigs-kiss.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +use Option instead of undefined | A diff --git a/.repos/effect-smol/.changeset/fluffy-meals-matter.md b/.repos/effect-smol/.changeset/fluffy-meals-matter.md new file mode 100644 index 00000000000..fa656127ba8 --- /dev/null +++ b/.repos/effect-smol/.changeset/fluffy-meals-matter.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +add OpenAiClient.withWebSocketMode diff --git a/.repos/effect-smol/.changeset/fluffy-pumas-push.md b/.repos/effect-smol/.changeset/fluffy-pumas-push.md new file mode 100644 index 00000000000..64e2b1e01ed --- /dev/null +++ b/.repos/effect-smol/.changeset/fluffy-pumas-push.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +disable sql traces for EventLog, RunnerStorage diff --git a/.repos/effect-smol/.changeset/forked-memo-maps.md b/.repos/effect-smol/.changeset/forked-memo-maps.md new file mode 100644 index 00000000000..37dd43fabc5 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/forty-hounds-cheer.md b/.repos/effect-smol/.changeset/forty-hounds-cheer.md new file mode 100644 index 00000000000..ffbf6d29d27 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/forty-otters-cry.md b/.repos/effect-smol/.changeset/forty-otters-cry.md new file mode 100644 index 00000000000..e95732ebb71 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/forty-rings-film.md b/.repos/effect-smol/.changeset/forty-rings-film.md new file mode 100644 index 00000000000..7c66c965d44 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/forty-signs-stay.md b/.repos/effect-smol/.changeset/forty-signs-stay.md new file mode 100644 index 00000000000..089873c775c --- /dev/null +++ b/.repos/effect-smol/.changeset/forty-signs-stay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add rpc ConnectionHooks diff --git a/.repos/effect-smol/.changeset/forty-swans-divide.md b/.repos/effect-smol/.changeset/forty-swans-divide.md new file mode 100644 index 00000000000..98e79f33d5f --- /dev/null +++ b/.repos/effect-smol/.changeset/forty-swans-divide.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +improve Schema.TaggedUnion .match auto completion diff --git a/.repos/effect-smol/.changeset/forty-trees-pay.md b/.repos/effect-smol/.changeset/forty-trees-pay.md new file mode 100644 index 00000000000..5e659393e51 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/four-papayas-bow.md b/.repos/effect-smol/.changeset/four-papayas-bow.md new file mode 100644 index 00000000000..a925f5442cb --- /dev/null +++ b/.repos/effect-smol/.changeset/four-papayas-bow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Support toolkit unions in `LanguageModel` options. diff --git a/.repos/effect-smol/.changeset/four-points-repeat.md b/.repos/effect-smol/.changeset/four-points-repeat.md new file mode 100644 index 00000000000..62f6b4b56ee --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fresh-cats-smash.md b/.repos/effect-smol/.changeset/fresh-cats-smash.md new file mode 100644 index 00000000000..d563083f4c4 --- /dev/null +++ b/.repos/effect-smol/.changeset/fresh-cats-smash.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix stream requests in Entity.toLayerQueue diff --git a/.repos/effect-smol/.changeset/fresh-emus-cheat.md b/.repos/effect-smol/.changeset/fresh-emus-cheat.md new file mode 100644 index 00000000000..3ee2e11d0b4 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fresh-monkeys-smoke.md b/.repos/effect-smol/.changeset/fresh-monkeys-smoke.md new file mode 100644 index 00000000000..523ab35adfa --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fruity-houses-learn.md b/.repos/effect-smol/.changeset/fruity-houses-learn.md new file mode 100644 index 00000000000..3e843ad2aff --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/full-adults-double.md b/.repos/effect-smol/.changeset/full-adults-double.md new file mode 100644 index 00000000000..d1d236aff30 --- /dev/null +++ b/.repos/effect-smol/.changeset/full-adults-double.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add RpcGroup.omit diff --git a/.repos/effect-smol/.changeset/funny-crabs-hang.md b/.repos/effect-smol/.changeset/funny-crabs-hang.md new file mode 100644 index 00000000000..2fd2a4cb787 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/funny-forks-move.md b/.repos/effect-smol/.changeset/funny-forks-move.md new file mode 100644 index 00000000000..b4f8cd507a2 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fuzzy-camels-hunt.md b/.repos/effect-smol/.changeset/fuzzy-camels-hunt.md new file mode 100644 index 00000000000..969059757c4 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fuzzy-dodos-help.md b/.repos/effect-smol/.changeset/fuzzy-dodos-help.md new file mode 100644 index 00000000000..9e2d5bddcc7 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fuzzy-lions-perform.md b/.repos/effect-smol/.changeset/fuzzy-lions-perform.md new file mode 100644 index 00000000000..90ec3708905 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/fuzzy-planets-sneeze.md b/.repos/effect-smol/.changeset/fuzzy-planets-sneeze.md new file mode 100644 index 00000000000..7e10603c94e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/gold-meteors-move.md b/.repos/effect-smol/.changeset/gold-meteors-move.md new file mode 100644 index 00000000000..62f8e5045b3 --- /dev/null +++ b/.repos/effect-smol/.changeset/gold-meteors-move.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow using Duration.Input with accessors diff --git a/.repos/effect-smol/.changeset/gold-readers-hug.md b/.repos/effect-smol/.changeset/gold-readers-hug.md new file mode 100644 index 00000000000..c1e74afbbf0 --- /dev/null +++ b/.repos/effect-smol/.changeset/gold-readers-hug.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +add .reactive to indexeddb .first queries diff --git a/.repos/effect-smol/.changeset/gold-rings-start.md b/.repos/effect-smol/.changeset/gold-rings-start.md new file mode 100644 index 00000000000..a47549eefae --- /dev/null +++ b/.repos/effect-smol/.changeset/gold-rings-start.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix Chat constructor types diff --git a/.repos/effect-smol/.changeset/good-tools-work.md b/.repos/effect-smol/.changeset/good-tools-work.md new file mode 100644 index 00000000000..7da12bb9e61 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/great-trains-mate.md b/.repos/effect-smol/.changeset/great-trains-mate.md new file mode 100644 index 00000000000..043f0ee30da --- /dev/null +++ b/.repos/effect-smol/.changeset/great-trains-mate.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix Duration.toMillis regression diff --git a/.repos/effect-smol/.changeset/great-trams-report.md b/.repos/effect-smol/.changeset/great-trams-report.md new file mode 100644 index 00000000000..ac694509186 --- /dev/null +++ b/.repos/effect-smol/.changeset/great-trams-report.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +cleanup IndexedDb prototypes diff --git a/.repos/effect-smol/.changeset/green-beds-unref.md b/.repos/effect-smol/.changeset/green-beds-unref.md new file mode 100644 index 00000000000..db4ec3b3d25 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/green-chips-wash.md b/.repos/effect-smol/.changeset/green-chips-wash.md new file mode 100644 index 00000000000..3b396434ec6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/green-moons-smile.md b/.repos/effect-smol/.changeset/green-moons-smile.md new file mode 100644 index 00000000000..6f3a83232cd --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/green-pugs-play.md b/.repos/effect-smol/.changeset/green-pugs-play.md new file mode 100644 index 00000000000..4b135069a4e --- /dev/null +++ b/.repos/effect-smol/.changeset/green-pugs-play.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +add defaults to indexeddb reactivity keys diff --git a/.repos/effect-smol/.changeset/green-rings-prove.md b/.repos/effect-smol/.changeset/green-rings-prove.md new file mode 100644 index 00000000000..c7a2ea6063d --- /dev/null +++ b/.repos/effect-smol/.changeset/green-rings-prove.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +update Model uuid helpers diff --git a/.repos/effect-smol/.changeset/happy-mirrors-dream.md b/.repos/effect-smol/.changeset/happy-mirrors-dream.md new file mode 100644 index 00000000000..b944244b531 --- /dev/null +++ b/.repos/effect-smol/.changeset/happy-mirrors-dream.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Socket.make diff --git a/.repos/effect-smol/.changeset/heavy-loops-cut.md b/.repos/effect-smol/.changeset/heavy-loops-cut.md new file mode 100644 index 00000000000..0e9730f7fc0 --- /dev/null +++ b/.repos/effect-smol/.changeset/heavy-loops-cut.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Unwrap `_Success` schema to enable field access. diff --git a/.repos/effect-smol/.changeset/heavy-trams-fix.md b/.repos/effect-smol/.changeset/heavy-trams-fix.md new file mode 100644 index 00000000000..50776967722 --- /dev/null +++ b/.repos/effect-smol/.changeset/heavy-trams-fix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +make HttpClientResponse pipeable diff --git a/.repos/effect-smol/.changeset/hip-socks-travel.md b/.repos/effect-smol/.changeset/hip-socks-travel.md new file mode 100644 index 00000000000..3f67b064b90 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/honest-pens-thank.md b/.repos/effect-smol/.changeset/honest-pens-thank.md new file mode 100644 index 00000000000..d5f0ee6e045 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/honest-rivers-notice.md b/.repos/effect-smol/.changeset/honest-rivers-notice.md new file mode 100644 index 00000000000..f3da306ce26 --- /dev/null +++ b/.repos/effect-smol/.changeset/honest-rivers-notice.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +seperate scheduler dispatch from yield decisions diff --git a/.repos/effect-smol/.changeset/hot-taxis-fry.md b/.repos/effect-smol/.changeset/hot-taxis-fry.md new file mode 100644 index 00000000000..2563a31ed6f --- /dev/null +++ b/.repos/effect-smol/.changeset/hot-taxis-fry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix issue with exported CLI `Completions` types diff --git a/.repos/effect-smol/.changeset/httpapi-endpoint-relax-constraints.md b/.repos/effect-smol/.changeset/httpapi-endpoint-relax-constraints.md new file mode 100644 index 00000000000..b7bc906d7d8 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/huge-moons-rhyme.md b/.repos/effect-smol/.changeset/huge-moons-rhyme.md new file mode 100644 index 00000000000..5488d582201 --- /dev/null +++ b/.repos/effect-smol/.changeset/huge-moons-rhyme.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +feat: Support Reference classes diff --git a/.repos/effect-smol/.changeset/humble-pigs-dig.md b/.repos/effect-smol/.changeset/humble-pigs-dig.md new file mode 100644 index 00000000000..212d8895e31 --- /dev/null +++ b/.repos/effect-smol/.changeset/humble-pigs-dig.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +align HttpClientRequest constructors with http method names diff --git a/.repos/effect-smol/.changeset/icy-flies-cross.md b/.repos/effect-smol/.changeset/icy-flies-cross.md new file mode 100644 index 00000000000..a6272595b3b --- /dev/null +++ b/.repos/effect-smol/.changeset/icy-flies-cross.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +update dependencies diff --git a/.repos/effect-smol/.changeset/itchy-radios-poke.md b/.repos/effect-smol/.changeset/itchy-radios-poke.md new file mode 100644 index 00000000000..0ab497b0873 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/itchy-results-bet.md b/.repos/effect-smol/.changeset/itchy-results-bet.md new file mode 100644 index 00000000000..d3998b75efb --- /dev/null +++ b/.repos/effect-smol/.changeset/itchy-results-bet.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure non-middleware http errors are correctly handled diff --git a/.repos/effect-smol/.changeset/itchy-shrimps-deny.md b/.repos/effect-smol/.changeset/itchy-shrimps-deny.md new file mode 100644 index 00000000000..bb120fb383e --- /dev/null +++ b/.repos/effect-smol/.changeset/itchy-shrimps-deny.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add ErrorReporter module diff --git a/.repos/effect-smol/.changeset/itchy-toes-promise.md b/.repos/effect-smol/.changeset/itchy-toes-promise.md new file mode 100644 index 00000000000..2c8c704b10f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/k8s-last-transition-null.md b/.repos/effect-smol/.changeset/k8s-last-transition-null.md new file mode 100644 index 00000000000..d55d7eceaf6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/khaki-cats-learn.md b/.repos/effect-smol/.changeset/khaki-cats-learn.md new file mode 100644 index 00000000000..5a017b62b22 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/khaki-melons-appear.md b/.repos/effect-smol/.changeset/khaki-melons-appear.md new file mode 100644 index 00000000000..72a00cf25dd --- /dev/null +++ b/.repos/effect-smol/.changeset/khaki-melons-appear.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix RpcWorker Protocol service key diff --git a/.repos/effect-smol/.changeset/kind-hounds-float.md b/.repos/effect-smol/.changeset/kind-hounds-float.md new file mode 100644 index 00000000000..b108a209522 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/kind-windows-fall.md b/.repos/effect-smol/.changeset/kind-windows-fall.md new file mode 100644 index 00000000000..3786a89ca6f --- /dev/null +++ b/.repos/effect-smol/.changeset/kind-windows-fall.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +rename WorkflowEngine.layer diff --git a/.repos/effect-smol/.changeset/late-hotels-rule.md b/.repos/effect-smol/.changeset/late-hotels-rule.md new file mode 100644 index 00000000000..4d7fc9e38b5 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/late-lamps-care.md b/.repos/effect-smol/.changeset/late-lamps-care.md new file mode 100644 index 00000000000..cabcf77eb22 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/late-rivers-applaud.md b/.repos/effect-smol/.changeset/late-rivers-applaud.md new file mode 100644 index 00000000000..37170d6b8fc --- /dev/null +++ b/.repos/effect-smol/.changeset/late-rivers-applaud.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: relax `asserts` and `is` constraints. diff --git a/.repos/effect-smol/.changeset/layer-map-dynamic-idle-ttl.md b/.repos/effect-smol/.changeset/layer-map-dynamic-idle-ttl.md new file mode 100644 index 00000000000..8a2532a9061 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/lazy-queens-rush.md b/.repos/effect-smol/.changeset/lazy-queens-rush.md new file mode 100644 index 00000000000..616b7a5cb22 --- /dev/null +++ b/.repos/effect-smol/.changeset/lazy-queens-rush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Context.mutate diff --git a/.repos/effect-smol/.changeset/lazy-recursive-forward-refs.md b/.repos/effect-smol/.changeset/lazy-recursive-forward-refs.md new file mode 100644 index 00000000000..29e13a726a8 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/lazy-timers-exist.md b/.repos/effect-smol/.changeset/lazy-timers-exist.md new file mode 100644 index 00000000000..e66de3a25d7 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/legal-pants-drop.md b/.repos/effect-smol/.changeset/legal-pants-drop.md new file mode 100644 index 00000000000..9bd6f6c6b7b --- /dev/null +++ b/.repos/effect-smol/.changeset/legal-pants-drop.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +remove body restriction for HttpClientRequest's diff --git a/.repos/effect-smol/.changeset/lemon-taxis-sin.md b/.repos/effect-smol/.changeset/lemon-taxis-sin.md new file mode 100644 index 00000000000..750c3254115 --- /dev/null +++ b/.repos/effect-smol/.changeset/lemon-taxis-sin.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +make HttpApi schema errors defects unless transformed diff --git a/.repos/effect-smol/.changeset/light-kids-sneeze.md b/.repos/effect-smol/.changeset/light-kids-sneeze.md new file mode 100644 index 00000000000..926ba7aaf9e --- /dev/null +++ b/.repos/effect-smol/.changeset/light-kids-sneeze.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +omit scope from HttpApi handlers diff --git a/.repos/effect-smol/.changeset/little-dryers-allow.md b/.repos/effect-smol/.changeset/little-dryers-allow.md new file mode 100644 index 00000000000..9211c900843 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/long-cameras-think.md b/.repos/effect-smol/.changeset/long-cameras-think.md new file mode 100644 index 00000000000..aafbd1f5cf4 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/lovely-cobras-change.md b/.repos/effect-smol/.changeset/lovely-cobras-change.md new file mode 100644 index 00000000000..e15d453c840 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/lovely-frogs-rescue.md b/.repos/effect-smol/.changeset/lovely-frogs-rescue.md new file mode 100644 index 00000000000..eb7ae29c929 --- /dev/null +++ b/.repos/effect-smol/.changeset/lovely-frogs-rescue.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow mcp errors to be encoded correctly diff --git a/.repos/effect-smol/.changeset/lucky-buttons-jump.md b/.repos/effect-smol/.changeset/lucky-buttons-jump.md new file mode 100644 index 00000000000..771c41ac399 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/lucky-phones-listen.md b/.repos/effect-smol/.changeset/lucky-phones-listen.md new file mode 100644 index 00000000000..0635e0afd70 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/lucky-worms-type.md b/.repos/effect-smol/.changeset/lucky-worms-type.md new file mode 100644 index 00000000000..d4814ccfbf9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/mean-dingos-share.md b/.repos/effect-smol/.changeset/mean-dingos-share.md new file mode 100644 index 00000000000..2fd4aef3893 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/mean-trains-smash.md b/.repos/effect-smol/.changeset/mean-trains-smash.md new file mode 100644 index 00000000000..0054e60435b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/metal-parts-yell.md b/.repos/effect-smol/.changeset/metal-parts-yell.md new file mode 100644 index 00000000000..a6d67450a10 --- /dev/null +++ b/.repos/effect-smol/.changeset/metal-parts-yell.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +simplify http logger disabling diff --git a/.repos/effect-smol/.changeset/mighty-games-matter.md b/.repos/effect-smol/.changeset/mighty-games-matter.md new file mode 100644 index 00000000000..d536ab3504d --- /dev/null +++ b/.repos/effect-smol/.changeset/mighty-games-matter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +remove use of bigint literals diff --git a/.repos/effect-smol/.changeset/nasty-geese-grow.md b/.repos/effect-smol/.changeset/nasty-geese-grow.md new file mode 100644 index 00000000000..ed408dc284a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/neat-goats-wave.md b/.repos/effect-smol/.changeset/neat-goats-wave.md new file mode 100644 index 00000000000..bb3f2c4f7be --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/neat-kings-chew.md b/.repos/effect-smol/.changeset/neat-kings-chew.md new file mode 100644 index 00000000000..1986ccb6c39 --- /dev/null +++ b/.repos/effect-smol/.changeset/neat-kings-chew.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +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-smol/.changeset/neat-lions-rest.md b/.repos/effect-smol/.changeset/neat-lions-rest.md new file mode 100644 index 00000000000..e0aa7c3fc62 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/neat-snails-wash.md b/.repos/effect-smol/.changeset/neat-snails-wash.md new file mode 100644 index 00000000000..3ab4515df22 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/neat-taxis-notice.md b/.repos/effect-smol/.changeset/neat-taxis-notice.md new file mode 100644 index 00000000000..ffdd50ed446 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/new-dogs-swim.md b/.repos/effect-smol/.changeset/new-dogs-swim.md new file mode 100644 index 00000000000..ef306ca7bb5 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/new-toes-stop.md b/.repos/effect-smol/.changeset/new-toes-stop.md new file mode 100644 index 00000000000..4270fd00cce --- /dev/null +++ b/.repos/effect-smol/.changeset/new-toes-stop.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +add idb stream and offset diff --git a/.repos/effect-smol/.changeset/ninety-geese-exist.md b/.repos/effect-smol/.changeset/ninety-geese-exist.md new file mode 100644 index 00000000000..20e8ce1b50a --- /dev/null +++ b/.repos/effect-smol/.changeset/ninety-geese-exist.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add HttpApiSecurity.http for passing custom schemes diff --git a/.repos/effect-smol/.changeset/odd-boats-think.md b/.repos/effect-smol/.changeset/odd-boats-think.md new file mode 100644 index 00000000000..7319b914e53 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/odd-bulldogs-sleep.md b/.repos/effect-smol/.changeset/odd-bulldogs-sleep.md new file mode 100644 index 00000000000..baf880811e7 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/odd-fans-glow.md b/.repos/effect-smol/.changeset/odd-fans-glow.md new file mode 100644 index 00000000000..ac7a1a804d8 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/odd-forks-talk.md b/.repos/effect-smol/.changeset/odd-forks-talk.md new file mode 100644 index 00000000000..150e017883b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/odd-laws-draw.md b/.repos/effect-smol/.changeset/odd-laws-draw.md new file mode 100644 index 00000000000..695ffcefd6e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/odd-owls-smoke.md b/.repos/effect-smol/.changeset/odd-owls-smoke.md new file mode 100644 index 00000000000..7085a3b5926 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/odd-socks-boil.md b/.repos/effect-smol/.changeset/odd-socks-boil.md new file mode 100644 index 00000000000..af72ca9828c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/odd-suns-dance.md b/.repos/effect-smol/.changeset/odd-suns-dance.md new file mode 100644 index 00000000000..226e74ffdbf --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/old-brooms-cry.md b/.repos/effect-smol/.changeset/old-brooms-cry.md new file mode 100644 index 00000000000..525a7a8f619 --- /dev/null +++ b/.repos/effect-smol/.changeset/old-brooms-cry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Filter.reason api diff --git a/.repos/effect-smol/.changeset/old-facts-stand.md b/.repos/effect-smol/.changeset/old-facts-stand.md new file mode 100644 index 00000000000..99a811620ab --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/old-mirrors-float.md b/.repos/effect-smol/.changeset/old-mirrors-float.md new file mode 100644 index 00000000000..a50aab5e731 --- /dev/null +++ b/.repos/effect-smol/.changeset/old-mirrors-float.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +make Layer.mock work with Stream and Channel diff --git a/.repos/effect-smol/.changeset/olive-poems-visit.md b/.repos/effect-smol/.changeset/olive-poems-visit.md new file mode 100644 index 00000000000..f09144e38a2 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/open-hotels-remain.md b/.repos/effect-smol/.changeset/open-hotels-remain.md new file mode 100644 index 00000000000..633584ceadc --- /dev/null +++ b/.repos/effect-smol/.changeset/open-hotels-remain.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +extend McpSchema to work with extensions diff --git a/.repos/effect-smol/.changeset/perfect-buckets-tickle.md b/.repos/effect-smol/.changeset/perfect-buckets-tickle.md new file mode 100644 index 00000000000..a17e31a516d --- /dev/null +++ b/.repos/effect-smol/.changeset/perfect-buckets-tickle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add default value support to `Prompt.file`. diff --git a/.repos/effect-smol/.changeset/petite-months-allow.md b/.repos/effect-smol/.changeset/petite-months-allow.md new file mode 100644 index 00000000000..04ad2d0b60c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/platform-crypto-service.md b/.repos/effect-smol/.changeset/platform-crypto-service.md new file mode 100644 index 00000000000..afbf216e6cb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/platform-node-shared-barrel.md b/.repos/effect-smol/.changeset/platform-node-shared-barrel.md new file mode 100644 index 00000000000..f73cae68443 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/plenty-moons-pull.md b/.repos/effect-smol/.changeset/plenty-moons-pull.md new file mode 100644 index 00000000000..adac8d000ff --- /dev/null +++ b/.repos/effect-smol/.changeset/plenty-moons-pull.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +use encoded types for idb queries diff --git a/.repos/effect-smol/.changeset/polite-brooms-tickle.md b/.repos/effect-smol/.changeset/polite-brooms-tickle.md new file mode 100644 index 00000000000..89a9b755ea3 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/polite-pigs-speak.md b/.repos/effect-smol/.changeset/polite-pigs-speak.md new file mode 100644 index 00000000000..44d427f9dc7 --- /dev/null +++ b/.repos/effect-smol/.changeset/polite-pigs-speak.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +use .toJSON for default .toString implementations diff --git a/.repos/effect-smol/.changeset/polite-tables-kneel.md b/.repos/effect-smol/.changeset/polite-tables-kneel.md new file mode 100644 index 00000000000..e80db998f1c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/port-react-hydration.md b/.repos/effect-smol/.changeset/port-react-hydration.md new file mode 100644 index 00000000000..a7cd38e2f11 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/pre.json b/.repos/effect-smol/.changeset/pre.json new file mode 100644 index 00000000000..64ca47b281f --- /dev/null +++ b/.repos/effect-smol/.changeset/pre.json @@ -0,0 +1,546 @@ +{ + "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-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-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", + "afraid-cobras-like", + "ai-openai-config-field-leak", + "ai-openai-file-nullable-fields", + "asyncresult-exhaustive", + "atom-stream-error-type", + "beige-paths-sort", + "better-apples-nail", + "better-rocks-arrive", + "big-pans-look", + "blue-dingos-greet", + "blue-ligers-cheat", + "blue-onions-smile", + "blue-ravens-type", + "blue-trams-kiss", + "bold-chairs-yawn", + "breezy-meals-see", + "bright-bugs-bow", + "bright-canyons-clean", + "bright-dogs-fail", + "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-panthers-nail", + "calm-squids-hug", + "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-tires-guess", + "clear-spies-boil", + "cli-help-choices", + "cold-knives-lie", + "cold-rooms-show", + "cold-sloths-wave", + "compact-json-schema-enum", + "config-withdefault-eager", + "consolidate-encoding", + "consolidate-sql-error", + "crisp-seas-warn", + "cuddly-rooms-bet", + "curly-poems-talk", + "curly-spies-relax", + "curvy-apples-float", + "custom-http-security-openapi-generator", + "cute-heads-thank", + "cyan-loops-grow", + "cyan-radios-switch", + "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", + "eight-turkeys-own", + "eighty-lies-deny", + "eighty-poets-draw", + "eighty-swans-scream", + "eighty-teeth-sniff", + "eleven-apes-share", + "eleven-numbers-bake", + "empty-gifts-beg", + "empty-http-rpc-client", + "eventlog-unencrypted", + "expand-schema-filter-output", + "export-schema-encode-keys-interface", + "extract-semaphore-latch", + "fair-bees-relax", + "fair-buttons-share", + "fair-cooks-stop", + "fair-cups-train", + "fair-dryers-speak", + "fair-forks-shake", + "fair-pandas-prove", + "fair-pants-float", + "fair-poems-visit", + "famous-wolves-lead", + "fancy-glasses-grow", + "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-ai-empty-params-structured-output", + "fix-ai-text-toolkit-typing", + "fix-catch-orelse-error-erasure", + "fix-class-constructor-defaults", + "fix-cli-mixed-global-flag-context", + "fix-config-withDefault", + "fix-datetime-gmt", + "fix-devtools-flush-on-teardown", + "fix-entity-proxy-rpc-handler-context", + "fix-entity-proxy-server-path-params", + "fix-has-interrupts-only-empty", + "fix-hashmap-bit31-ordering", + "fix-headers-proto-enumerability", + "fix-http-incoming-message-parse-options", + "fix-httpapi-security-middleware-cache", + "fix-is-json-dag", + "fix-json-schema-anyof-oneof-siblings", + "fix-keepalive-blocked-timers", + "fix-mcp-param-name-resolution", + "fix-mermaid-escape-special-chars", + "fix-mutable-list-filter-length", + "fix-openai-mcp-tool-names", + "fix-openapi-generator-form-urlencoded", + "fix-openapi-preserve-multiple-response-content-types", + "fix-openrouter-sparse-array", + "fix-otel-logger-clock-skew", + "fix-otel-logger-severity-number", + "fix-queue-collect-duplication", + "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-schedule-fixed-double-exec", + "fix-schedule-reduce-sync-state", + "fix-schema-arbitrary-exclusive-bounds", + "fix-schema-encodekeys-class", + "fix-schema-encodekeys-struct", + "fix-schema-identifier-expected-message", + "fix-schema-is-uuid", + "fix-searchparam-initial-decode", + "fix-serializable-wire-transfer", + "fix-stream-grouped-within-flush", + "fix-stream-scan-effect", + "fix-stream-scoped-scope", + "fix-strip-approval-artifacts-multi-round", + "fix-struct-utility-types-simplify", + "fix-tagged-union-class-sentinels", + "fix-tagged-union-match-unify", + "fix-tuple-with-rest-post-rest-index-drift", + "fix-tuple-with-rest-post-rest-validation", + "fix-types-voidifempty", + "fix-void-response-encoding", + "fix-workflow-proxy-rpc-handler-context", + "flat-chicken-remain", + "floppy-cows-spend", + "floppy-items-admire", + "floppy-pigs-kiss", + "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-emus-cheat", + "fresh-monkeys-smoke", + "fruity-houses-learn", + "full-adults-double", + "funny-crabs-hang", + "funny-forks-move", + "fuzzy-camels-hunt", + "fuzzy-dodos-help", + "fuzzy-lions-perform", + "fuzzy-planets-sneeze", + "gold-meteors-move", + "gold-readers-hug", + "gold-rings-start", + "good-tools-work", + "great-trains-mate", + "great-trams-report", + "green-beds-unref", + "green-chips-wash", + "green-moons-smile", + "green-pugs-play", + "green-rings-prove", + "happy-mirrors-dream", + "heavy-loops-cut", + "heavy-trams-fix", + "hip-socks-travel", + "honest-pens-thank", + "honest-rivers-notice", + "hot-taxis-fry", + "httpapi-endpoint-relax-constraints", + "huge-moons-rhyme", + "humble-pigs-dig", + "icy-flies-cross", + "itchy-radios-poke", + "itchy-results-bet", + "itchy-shrimps-deny", + "itchy-toes-promise", + "k8s-last-transition-null", + "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", + "mean-dingos-share", + "mean-trains-smash", + "metal-parts-yell", + "mighty-games-matter", + "nasty-geese-grow", + "neat-goats-wave", + "neat-kings-chew", + "neat-lions-rest", + "neat-snails-wash", + "neat-taxis-notice", + "new-dogs-swim", + "new-toes-stop", + "ninety-geese-exist", + "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", + "open-hotels-remain", + "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", + "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-radios-wave", + "ready-olives-divide", + "real-trains-ring", + "red-pigs-repair", + "refactor-cli-global-flags", + "refactor-representation-references", + "remove-effect-transactionwith", + "remove-http-span-counter", + "remove-nullor", + "remove-openapi-fromapi-options", + "remove-unused-utils-apis", + "rename-rebuild-out", + "restore-schema-parse-options", + "rich-dots-push", + "rich-hoops-nail", + "rich-sloths-draw", + "ripe-lies-battle", + "rpc-middleware-provides-fix", + "schema-as-class", + "schema-asserts-signature", + "schema-clean-up-additionalProperties", + "schema-datetime-utc-from-string", + "schema-decoding-defaults-services", + "schema-defaults-issue-channel", + "schema-dollar-prefix", + "schema-missing-self-generic", + "schema-refactor-toCodecJson", + "schema-remove-annotate-in", + "schema-rename-makeUnsafe-to-make", + "schema-rename-parser-makeUnsafe", + "schema-result-combinators", + "schema-struct-simplify", + "seven-mugs-marry", + "shaggy-birds-stay", + "shaggy-cities-push", + "shaggy-numbers-accept", + "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", + "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", + "sixty-mails-shout", + "sixty-socks-yell", + "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-pumas-prove", + "soft-comics-wink", + "soft-delete-sqlmodel", + "soft-seals-allow", + "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", + "stale-dots-tell", + "stale-snakes-know", + "strict-areas-end", + "strict-buckets-hug", + "strip-resolved-approvals", + "strong-balloons-tickle", + "strong-bees-queue", + "struct-record", + "sunny-ads-hang", + "sunny-bikes-sleep", + "sunny-rooms-invent", + "sweet-donuts-bet", + "sweet-hotels-give", + "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", + "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-icons-glow", + "tidy-stars-drive", + "tiny-buckets-wave", + "tiny-lilies-flash", + "tiny-rabbits-smile", + "tocodecjson-return-json-type", + "tool-get-json-schema-tests", + "true-actors-battle", + "twenty-buttons-cheer", + "two-roses-double", + "upset-colts-stick", + "vast-bananas-send", + "vast-deserts-travel", + "violet-peaches-feel", + "vitest-layer-top-level-options", + "wacky-grapes-poke", + "wacky-rice-add", + "warm-friends-tie", + "warm-snails-shop", + "wet-news-invent", + "wild-readers-clean", + "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-smol/.changeset/public-deer-ring.md b/.repos/effect-smol/.changeset/public-deer-ring.md new file mode 100644 index 00000000000..251292bbeb2 --- /dev/null +++ b/.repos/effect-smol/.changeset/public-deer-ring.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add WorkflowEngine interruptUnsafe diff --git a/.repos/effect-smol/.changeset/public-jeans-stop.md b/.repos/effect-smol/.changeset/public-jeans-stop.md new file mode 100644 index 00000000000..719c4407cd7 --- /dev/null +++ b/.repos/effect-smol/.changeset/public-jeans-stop.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +reset openai websocket on error diff --git a/.repos/effect-smol/.changeset/pubsub-publish-false.md b/.repos/effect-smol/.changeset/pubsub-publish-false.md new file mode 100644 index 00000000000..577022ba2f4 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/puny-pens-clap.md b/.repos/effect-smol/.changeset/puny-pens-clap.md new file mode 100644 index 00000000000..c3087e0b6ab --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/purple-bars-prove.md b/.repos/effect-smol/.changeset/purple-bars-prove.md new file mode 100644 index 00000000000..ca16c966999 --- /dev/null +++ b/.repos/effect-smol/.changeset/purple-bars-prove.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +bump undici versions diff --git a/.repos/effect-smol/.changeset/purple-schools-float.md b/.repos/effect-smol/.changeset/purple-schools-float.md new file mode 100644 index 00000000000..caac67d563a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/purple-turtles-draw.md b/.repos/effect-smol/.changeset/purple-turtles-draw.md new file mode 100644 index 00000000000..95ad9fd7c80 --- /dev/null +++ b/.repos/effect-smol/.changeset/purple-turtles-draw.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +add .reverse() to idb select diff --git a/.repos/effect-smol/.changeset/quick-dragons-fix.md b/.repos/effect-smol/.changeset/quick-dragons-fix.md new file mode 100644 index 00000000000..ded3d777915 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/quick-falcons-travel.md b/.repos/effect-smol/.changeset/quick-falcons-travel.md new file mode 100644 index 00000000000..62cc670ae56 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/quick-geese-relax.md b/.repos/effect-smol/.changeset/quick-geese-relax.md new file mode 100644 index 00000000000..d0d00ec174f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/quick-lamps-dig.md b/.repos/effect-smol/.changeset/quick-lamps-dig.md new file mode 100644 index 00000000000..c2398444f1e --- /dev/null +++ b/.repos/effect-smol/.changeset/quick-lamps-dig.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add HttpApiMiddleware.layerSchemaErrorTransform diff --git a/.repos/effect-smol/.changeset/quick-lizards-fall.md b/.repos/effect-smol/.changeset/quick-lizards-fall.md new file mode 100644 index 00000000000..10d387f4731 --- /dev/null +++ b/.repos/effect-smol/.changeset/quick-lizards-fall.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Add rebuild api to idb databases diff --git a/.repos/effect-smol/.changeset/quick-trees-join.md b/.repos/effect-smol/.changeset/quick-trees-join.md new file mode 100644 index 00000000000..623775195bf --- /dev/null +++ b/.repos/effect-smol/.changeset/quick-trees-join.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +remove all non-regional service usage diff --git a/.repos/effect-smol/.changeset/quiet-carpets-grin.md b/.repos/effect-smol/.changeset/quiet-carpets-grin.md new file mode 100644 index 00000000000..e3a4e437099 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/quiet-files-hunt.md b/.repos/effect-smol/.changeset/quiet-files-hunt.md new file mode 100644 index 00000000000..d534ce656e9 --- /dev/null +++ b/.repos/effect-smol/.changeset/quiet-files-hunt.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +wrap httpapi request context with HttpRouter.Request diff --git a/.repos/effect-smol/.changeset/quiet-radios-wave.md b/.repos/effect-smol/.changeset/quiet-radios-wave.md new file mode 100644 index 00000000000..fdbce5a8885 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/ready-olives-divide.md b/.repos/effect-smol/.changeset/ready-olives-divide.md new file mode 100644 index 00000000000..2502b4d1a35 --- /dev/null +++ b/.repos/effect-smol/.changeset/ready-olives-divide.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +handle openai ws error events diff --git a/.repos/effect-smol/.changeset/real-trains-ring.md b/.repos/effect-smol/.changeset/real-trains-ring.md new file mode 100644 index 00000000000..5cd40a0a3ac --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/red-pigs-repair.md b/.repos/effect-smol/.changeset/red-pigs-repair.md new file mode 100644 index 00000000000..005ae5f8881 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/refactor-cli-global-flags.md b/.repos/effect-smol/.changeset/refactor-cli-global-flags.md new file mode 100644 index 00000000000..ccfecb9289a --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/refactor-representation-references.md b/.repos/effect-smol/.changeset/refactor-representation-references.md new file mode 100644 index 00000000000..e72607fe0d3 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/remove-effect-transactionwith.md b/.repos/effect-smol/.changeset/remove-effect-transactionwith.md new file mode 100644 index 00000000000..5f5c63e2db3 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/remove-http-span-counter.md b/.repos/effect-smol/.changeset/remove-http-span-counter.md new file mode 100644 index 00000000000..e0458e74213 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/remove-nullor.md b/.repos/effect-smol/.changeset/remove-nullor.md new file mode 100644 index 00000000000..28617d3a3e7 --- /dev/null +++ b/.repos/effect-smol/.changeset/remove-nullor.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove unused `effect/NullOr` module. diff --git a/.repos/effect-smol/.changeset/remove-openapi-fromapi-options.md b/.repos/effect-smol/.changeset/remove-openapi-fromapi-options.md new file mode 100644 index 00000000000..97be4cd9a68 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/remove-unused-utils-apis.md b/.repos/effect-smol/.changeset/remove-unused-utils-apis.md new file mode 100644 index 00000000000..986dd0e0cc4 --- /dev/null +++ b/.repos/effect-smol/.changeset/remove-unused-utils-apis.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove unused APIs from the `Utils` module. diff --git a/.repos/effect-smol/.changeset/rename-rebuild-out.md b/.repos/effect-smol/.changeset/rename-rebuild-out.md new file mode 100644 index 00000000000..106bcc1e8ee --- /dev/null +++ b/.repos/effect-smol/.changeset/rename-rebuild-out.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: rename `"~rebuild.out"` to `"Rebuild"` diff --git a/.repos/effect-smol/.changeset/restore-schema-parse-options.md b/.repos/effect-smol/.changeset/restore-schema-parse-options.md new file mode 100644 index 00000000000..7a244549ab9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/rich-dots-push.md b/.repos/effect-smol/.changeset/rich-dots-push.md new file mode 100644 index 00000000000..3424b6148d6 --- /dev/null +++ b/.repos/effect-smol/.changeset/rich-dots-push.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix `isNullish()` type predicate diff --git a/.repos/effect-smol/.changeset/rich-hoops-nail.md b/.repos/effect-smol/.changeset/rich-hoops-nail.md new file mode 100644 index 00000000000..540a3d6aae9 --- /dev/null +++ b/.repos/effect-smol/.changeset/rich-hoops-nail.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +rename DurationInput to Duration.Input diff --git a/.repos/effect-smol/.changeset/rich-sloths-draw.md b/.repos/effect-smol/.changeset/rich-sloths-draw.md new file mode 100644 index 00000000000..98a0a994482 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/ripe-lies-battle.md b/.repos/effect-smol/.changeset/ripe-lies-battle.md new file mode 100644 index 00000000000..3f6b04677a2 --- /dev/null +++ b/.repos/effect-smol/.changeset/ripe-lies-battle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +EventLog Identity string encodes to base 64 diff --git a/.repos/effect-smol/.changeset/rpc-middleware-provides-fix.md b/.repos/effect-smol/.changeset/rpc-middleware-provides-fix.md new file mode 100644 index 00000000000..30a26567df3 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-as-class.md b/.repos/effect-smol/.changeset/schema-as-class.md new file mode 100644 index 00000000000..79572318c6f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-asserts-signature.md b/.repos/effect-smol/.changeset/schema-asserts-signature.md new file mode 100644 index 00000000000..173f65875fc --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-clean-up-additionalProperties.md b/.repos/effect-smol/.changeset/schema-clean-up-additionalProperties.md new file mode 100644 index 00000000000..06d3a85fe6e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-datetime-utc-from-string.md b/.repos/effect-smol/.changeset/schema-datetime-utc-from-string.md new file mode 100644 index 00000000000..bd52ade892b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-decoding-defaults-services.md b/.repos/effect-smol/.changeset/schema-decoding-defaults-services.md new file mode 100644 index 00000000000..a3bd9dc3773 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-defaults-issue-channel.md b/.repos/effect-smol/.changeset/schema-defaults-issue-channel.md new file mode 100644 index 00000000000..07a351b9ebb --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-dollar-prefix.md b/.repos/effect-smol/.changeset/schema-dollar-prefix.md new file mode 100644 index 00000000000..947fc16d682 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-missing-self-generic.md b/.repos/effect-smol/.changeset/schema-missing-self-generic.md new file mode 100644 index 00000000000..3e877851a59 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-refactor-toCodecJson.md b/.repos/effect-smol/.changeset/schema-refactor-toCodecJson.md new file mode 100644 index 00000000000..3bd03e70088 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-remove-annotate-in.md b/.repos/effect-smol/.changeset/schema-remove-annotate-in.md new file mode 100644 index 00000000000..93dd5bb124c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-rename-makeUnsafe-to-make.md b/.repos/effect-smol/.changeset/schema-rename-makeUnsafe-to-make.md new file mode 100644 index 00000000000..f2fa96b810e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-rename-parser-makeUnsafe.md b/.repos/effect-smol/.changeset/schema-rename-parser-makeUnsafe.md new file mode 100644 index 00000000000..7573aa817e0 --- /dev/null +++ b/.repos/effect-smol/.changeset/schema-rename-parser-makeUnsafe.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename `SchemaParser.makeUnsafe` to `SchemaParser.make`. diff --git a/.repos/effect-smol/.changeset/schema-result-combinators.md b/.repos/effect-smol/.changeset/schema-result-combinators.md new file mode 100644 index 00000000000..d313cf08796 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/schema-struct-simplify.md b/.repos/effect-smol/.changeset/schema-struct-simplify.md new file mode 100644 index 00000000000..c86120ae47b --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/seven-mugs-marry.md b/.repos/effect-smol/.changeset/seven-mugs-marry.md new file mode 100644 index 00000000000..fd3dd9d737f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/shaggy-birds-stay.md b/.repos/effect-smol/.changeset/shaggy-birds-stay.md new file mode 100644 index 00000000000..5834231b92a --- /dev/null +++ b/.repos/effect-smol/.changeset/shaggy-birds-stay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow passing void for request constructors diff --git a/.repos/effect-smol/.changeset/shaggy-cities-push.md b/.repos/effect-smol/.changeset/shaggy-cities-push.md new file mode 100644 index 00000000000..dfff5eac295 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/shaggy-numbers-accept.md b/.repos/effect-smol/.changeset/shaggy-numbers-accept.md new file mode 100644 index 00000000000..df7340ac1d9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/sharp-emus-applaud.md b/.repos/effect-smol/.changeset/sharp-emus-applaud.md new file mode 100644 index 00000000000..aa91f34010c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/sharp-goats-wink.md b/.repos/effect-smol/.changeset/sharp-goats-wink.md new file mode 100644 index 00000000000..19588ff9bf0 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/sharp-pandas-care.md b/.repos/effect-smol/.changeset/sharp-pandas-care.md new file mode 100644 index 00000000000..21c1cb90c21 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/sharp-peas-march.md b/.repos/effect-smol/.changeset/sharp-peas-march.md new file mode 100644 index 00000000000..7bce2812ccf --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/sharp-rules-draw.md b/.repos/effect-smol/.changeset/sharp-rules-draw.md new file mode 100644 index 00000000000..8246dde8abd --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/sharp-singers-sort.md b/.repos/effect-smol/.changeset/sharp-singers-sort.md new file mode 100644 index 00000000000..75c499727c5 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/shiny-trains-hug.md b/.repos/effect-smol/.changeset/shiny-trains-hug.md new file mode 100644 index 00000000000..451208484dc --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/short-cows-relate.md b/.repos/effect-smol/.changeset/short-cows-relate.md new file mode 100644 index 00000000000..2eb2ae0ad7c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/short-foxes-admire.md b/.repos/effect-smol/.changeset/short-foxes-admire.md new file mode 100644 index 00000000000..2da4d3a3dbf --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/shy-cycles-flow.md b/.repos/effect-smol/.changeset/shy-cycles-flow.md new file mode 100644 index 00000000000..60188e06984 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/shy-geckos-sniff.md b/.repos/effect-smol/.changeset/shy-geckos-sniff.md new file mode 100644 index 00000000000..bf5f07dcd6f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/silent-geckos-matter.md b/.repos/effect-smol/.changeset/silent-geckos-matter.md new file mode 100644 index 00000000000..df5aadf3ba8 --- /dev/null +++ b/.repos/effect-smol/.changeset/silent-geckos-matter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +compare transaction connections by reference diff --git a/.repos/effect-smol/.changeset/silent-needles-design.md b/.repos/effect-smol/.changeset/silent-needles-design.md new file mode 100644 index 00000000000..ee84c0eb70f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/silent-plants-matter.md b/.repos/effect-smol/.changeset/silent-plants-matter.md new file mode 100644 index 00000000000..e9609eab2ce --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/silent-spoons-stare.md b/.repos/effect-smol/.changeset/silent-spoons-stare.md new file mode 100644 index 00000000000..6d45025ee37 --- /dev/null +++ b/.repos/effect-smol/.changeset/silent-spoons-stare.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +catch errors in pullIntoWritable diff --git a/.repos/effect-smol/.changeset/silly-loops-tickle.md b/.repos/effect-smol/.changeset/silly-loops-tickle.md new file mode 100644 index 00000000000..a5493c8e05a --- /dev/null +++ b/.repos/effect-smol/.changeset/silly-loops-tickle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +DateTime.distance now returns a Duration diff --git a/.repos/effect-smol/.changeset/silver-bulk-indexeddb.md b/.repos/effect-smol/.changeset/silver-bulk-indexeddb.md new file mode 100644 index 00000000000..7137730b4a0 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/silver-emus-smoke.md b/.repos/effect-smol/.changeset/silver-emus-smoke.md new file mode 100644 index 00000000000..19f10e09d7d --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/silver-kings-poke.md b/.repos/effect-smol/.changeset/silver-kings-poke.md new file mode 100644 index 00000000000..11d14a6b50b --- /dev/null +++ b/.repos/effect-smol/.changeset/silver-kings-poke.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +simplify SubscriptionRef diff --git a/.repos/effect-smol/.changeset/silver-snails-sqlite.md b/.repos/effect-smol/.changeset/silver-snails-sqlite.md new file mode 100644 index 00000000000..dc4ddd43f76 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/silver-wings-watch.md b/.repos/effect-smol/.changeset/silver-wings-watch.md new file mode 100644 index 00000000000..3a1d49eaca0 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/six-cups-taste.md b/.repos/effect-smol/.changeset/six-cups-taste.md new file mode 100644 index 00000000000..71d570b69f6 --- /dev/null +++ b/.repos/effect-smol/.changeset/six-cups-taste.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add args to Stdio service diff --git a/.repos/effect-smol/.changeset/sixty-mails-shout.md b/.repos/effect-smol/.changeset/sixty-mails-shout.md new file mode 100644 index 00000000000..b4bff839575 --- /dev/null +++ b/.repos/effect-smol/.changeset/sixty-mails-shout.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix VariantSchema.Union diff --git a/.repos/effect-smol/.changeset/sixty-socks-yell.md b/.repos/effect-smol/.changeset/sixty-socks-yell.md new file mode 100644 index 00000000000..1441984b729 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/slimy-planets-divide.md b/.repos/effect-smol/.changeset/slimy-planets-divide.md new file mode 100644 index 00000000000..aed2e85f2e3 --- /dev/null +++ b/.repos/effect-smol/.changeset/slimy-planets-divide.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow assigning Temporal types to DateTime & Duration input diff --git a/.repos/effect-smol/.changeset/slimy-turtles-juggle.md b/.repos/effect-smol/.changeset/slimy-turtles-juggle.md new file mode 100644 index 00000000000..2cdb299ae2d --- /dev/null +++ b/.repos/effect-smol/.changeset/slimy-turtles-juggle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +refactor SqlSchema apis diff --git a/.repos/effect-smol/.changeset/slow-beans-battle.md b/.repos/effect-smol/.changeset/slow-beans-battle.md new file mode 100644 index 00000000000..e3a80a36916 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/slow-berries-enjoy.md b/.repos/effect-smol/.changeset/slow-berries-enjoy.md new file mode 100644 index 00000000000..de89cc726df --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/small-bugs-hunt.md b/.repos/effect-smol/.changeset/small-bugs-hunt.md new file mode 100644 index 00000000000..bad2315e98d --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/small-crabs-care.md b/.repos/effect-smol/.changeset/small-crabs-care.md new file mode 100644 index 00000000000..c79ad5cb4f7 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/small-pets-sit.md b/.repos/effect-smol/.changeset/small-pets-sit.md new file mode 100644 index 00000000000..5fd8650d520 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/smart-ducks-jump.md b/.repos/effect-smol/.changeset/smart-ducks-jump.md new file mode 100644 index 00000000000..2322e4f7ee9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/smart-pillows-buy.md b/.repos/effect-smol/.changeset/smart-pillows-buy.md new file mode 100644 index 00000000000..f4ac640fa55 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/smart-timers-fly.md b/.repos/effect-smol/.changeset/smart-timers-fly.md new file mode 100644 index 00000000000..e9a642781b5 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/smart-tips-sort.md b/.repos/effect-smol/.changeset/smart-tips-sort.md new file mode 100644 index 00000000000..c23b25c0dc8 --- /dev/null +++ b/.repos/effect-smol/.changeset/smart-tips-sort.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix Latch.release diff --git a/.repos/effect-smol/.changeset/social-pumas-prove.md b/.repos/effect-smol/.changeset/social-pumas-prove.md new file mode 100644 index 00000000000..1e71087bf37 --- /dev/null +++ b/.repos/effect-smol/.changeset/social-pumas-prove.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +disable tracer propagation for otlp exporter diff --git a/.repos/effect-smol/.changeset/soft-comics-wink.md b/.repos/effect-smol/.changeset/soft-comics-wink.md new file mode 100644 index 00000000000..e93cf52490f --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/soft-delete-sqlmodel.md b/.repos/effect-smol/.changeset/soft-delete-sqlmodel.md new file mode 100644 index 00000000000..08a90e58a50 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/soft-seals-allow.md b/.repos/effect-smol/.changeset/soft-seals-allow.md new file mode 100644 index 00000000000..54e3bc761cd --- /dev/null +++ b/.repos/effect-smol/.changeset/soft-seals-allow.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +cache base idb query builders diff --git a/.repos/effect-smol/.changeset/solid-doors-ring.md b/.repos/effect-smol/.changeset/solid-doors-ring.md new file mode 100644 index 00000000000..6178bf731f6 --- /dev/null +++ b/.repos/effect-smol/.changeset/solid-doors-ring.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow creating standalone http handlers from HttpApiEndpoints diff --git a/.repos/effect-smol/.changeset/solid-items-tease.md b/.repos/effect-smol/.changeset/solid-items-tease.md new file mode 100644 index 00000000000..ed43ea9b2e7 --- /dev/null +++ b/.repos/effect-smol/.changeset/solid-items-tease.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix atom node timeout cleanup diff --git a/.repos/effect-smol/.changeset/solid-towns-smoke.md b/.repos/effect-smol/.changeset/solid-towns-smoke.md new file mode 100644 index 00000000000..44300aae118 --- /dev/null +++ b/.repos/effect-smol/.changeset/solid-towns-smoke.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +rename Model.Generated to Model.GeneratedByDb diff --git a/.repos/effect-smol/.changeset/sour-canyons-rescue.md b/.repos/effect-smol/.changeset/sour-canyons-rescue.md new file mode 100644 index 00000000000..7499eba6bcc --- /dev/null +++ b/.repos/effect-smol/.changeset/sour-canyons-rescue.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add `useCodecs` option to HttpClientEndpoint constructors diff --git a/.repos/effect-smol/.changeset/sparkly-bears-act.md b/.repos/effect-smol/.changeset/sparkly-bears-act.md new file mode 100644 index 00000000000..655477a384b --- /dev/null +++ b/.repos/effect-smol/.changeset/sparkly-bears-act.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add `Effect.acquireDisposable` diff --git a/.repos/effect-smol/.changeset/sparkly-coins-sit.md b/.repos/effect-smol/.changeset/sparkly-coins-sit.md new file mode 100644 index 00000000000..2de14b62f55 --- /dev/null +++ b/.repos/effect-smol/.changeset/sparkly-coins-sit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +expose mcp client capabilities diff --git a/.repos/effect-smol/.changeset/spotty-comics-fry.md b/.repos/effect-smol/.changeset/spotty-comics-fry.md new file mode 100644 index 00000000000..496c87adf97 --- /dev/null +++ b/.repos/effect-smol/.changeset/spotty-comics-fry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix OpenApi Multipart file upload schema generation diff --git a/.repos/effect-smol/.changeset/sql-migrator-mjs-mts.md b/.repos/effect-smol/.changeset/sql-migrator-mjs-mts.md new file mode 100644 index 00000000000..071d942c8a1 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/stale-dots-tell.md b/.repos/effect-smol/.changeset/stale-dots-tell.md new file mode 100644 index 00000000000..c6609de6b06 --- /dev/null +++ b/.repos/effect-smol/.changeset/stale-dots-tell.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +improve HttpClient.withRateLimiter initial state tracking diff --git a/.repos/effect-smol/.changeset/stale-snakes-know.md b/.repos/effect-smol/.changeset/stale-snakes-know.md new file mode 100644 index 00000000000..a6272595b3b --- /dev/null +++ b/.repos/effect-smol/.changeset/stale-snakes-know.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +update dependencies diff --git a/.repos/effect-smol/.changeset/strict-areas-end.md b/.repos/effect-smol/.changeset/strict-areas-end.md new file mode 100644 index 00000000000..c469085e2f8 --- /dev/null +++ b/.repos/effect-smol/.changeset/strict-areas-end.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Track ManagedRuntime fibers in a scope diff --git a/.repos/effect-smol/.changeset/strict-buckets-hug.md b/.repos/effect-smol/.changeset/strict-buckets-hug.md new file mode 100644 index 00000000000..73164ef283d --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/strip-resolved-approvals.md b/.repos/effect-smol/.changeset/strip-resolved-approvals.md new file mode 100644 index 00000000000..4cdf441af04 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/strong-balloons-tickle.md b/.repos/effect-smol/.changeset/strong-balloons-tickle.md new file mode 100644 index 00000000000..c778264028c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/strong-bees-queue.md b/.repos/effect-smol/.changeset/strong-bees-queue.md new file mode 100644 index 00000000000..d7c401842d7 --- /dev/null +++ b/.repos/effect-smol/.changeset/strong-bees-queue.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add the unstable workflow DurableQueue module. diff --git a/.repos/effect-smol/.changeset/struct-record.md b/.repos/effect-smol/.changeset/struct-record.md new file mode 100644 index 00000000000..9a6d46a74cc --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/sunny-ads-hang.md b/.repos/effect-smol/.changeset/sunny-ads-hang.md new file mode 100644 index 00000000000..d51143ef68f --- /dev/null +++ b/.repos/effect-smol/.changeset/sunny-ads-hang.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add ClusterSchema.WithTransaction annotation diff --git a/.repos/effect-smol/.changeset/sunny-bikes-sleep.md b/.repos/effect-smol/.changeset/sunny-bikes-sleep.md new file mode 100644 index 00000000000..79412b327ec --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/sunny-rooms-invent.md b/.repos/effect-smol/.changeset/sunny-rooms-invent.md new file mode 100644 index 00000000000..4e6390169f9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/sweet-donuts-bet.md b/.repos/effect-smol/.changeset/sweet-donuts-bet.md new file mode 100644 index 00000000000..3d09822fc64 --- /dev/null +++ b/.repos/effect-smol/.changeset/sweet-donuts-bet.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add graceful shutdown to http servers diff --git a/.repos/effect-smol/.changeset/sweet-hotels-give.md b/.repos/effect-smol/.changeset/sweet-hotels-give.md new file mode 100644 index 00000000000..a54bddee0f2 --- /dev/null +++ b/.repos/effect-smol/.changeset/sweet-hotels-give.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +make EntityResource lazy by default diff --git a/.repos/effect-smol/.changeset/sweet-views-learn.md b/.repos/effect-smol/.changeset/sweet-views-learn.md new file mode 100644 index 00000000000..dda17ba48d6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/swift-spiders-unpack.md b/.repos/effect-smol/.changeset/swift-spiders-unpack.md new file mode 100644 index 00000000000..3f1f957bcd6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/swift-symbols-stand.md b/.repos/effect-smol/.changeset/swift-symbols-stand.md new file mode 100644 index 00000000000..2dbec991116 --- /dev/null +++ b/.repos/effect-smol/.changeset/swift-symbols-stand.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +remove rpc client nesting to improve type performance diff --git a/.repos/effect-smol/.changeset/tagged-error-class-optional-empty-props.md b/.repos/effect-smol/.changeset/tagged-error-class-optional-empty-props.md new file mode 100644 index 00000000000..7b63b56e58c --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tall-hairs-return.md b/.repos/effect-smol/.changeset/tall-hairs-return.md new file mode 100644 index 00000000000..551d144e9be --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tall-mails-listen.md b/.repos/effect-smol/.changeset/tall-mails-listen.md new file mode 100644 index 00000000000..96d7af428f2 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tall-queens-cheer.md b/.repos/effect-smol/.changeset/tall-queens-cheer.md new file mode 100644 index 00000000000..cffebd79521 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tall-wombats-wave.md b/.repos/effect-smol/.changeset/tall-wombats-wave.md new file mode 100644 index 00000000000..8bf297ccee8 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tangy-colts-lose.md b/.repos/effect-smol/.changeset/tangy-colts-lose.md new file mode 100644 index 00000000000..9678f5343aa --- /dev/null +++ b/.repos/effect-smol/.changeset/tangy-colts-lose.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +simplify NodeChildSpawner stdout streams diff --git a/.repos/effect-smol/.changeset/tasty-comics-send.md b/.repos/effect-smol/.changeset/tasty-comics-send.md new file mode 100644 index 00000000000..78bed2e7688 --- /dev/null +++ b/.repos/effect-smol/.changeset/tasty-comics-send.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +improve runSync error when executing async effects diff --git a/.repos/effect-smol/.changeset/ten-kings-fry.md b/.repos/effect-smol/.changeset/ten-kings-fry.md new file mode 100644 index 00000000000..593dc946006 --- /dev/null +++ b/.repos/effect-smol/.changeset/ten-kings-fry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix empty body decoding in HttpApiBuilder diff --git a/.repos/effect-smol/.changeset/thick-pandas-wait.md b/.repos/effect-smol/.changeset/thick-pandas-wait.md new file mode 100644 index 00000000000..5ddc8914517 --- /dev/null +++ b/.repos/effect-smol/.changeset/thick-pandas-wait.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add default value support to CLI integer prompts. diff --git a/.repos/effect-smol/.changeset/thin-ducks-wonder.md b/.repos/effect-smol/.changeset/thin-ducks-wonder.md new file mode 100644 index 00000000000..2176226f917 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/thirty-ducks-go.md b/.repos/effect-smol/.changeset/thirty-ducks-go.md new file mode 100644 index 00000000000..b3f3aa17e3b --- /dev/null +++ b/.repos/effect-smol/.changeset/thirty-ducks-go.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +HttpClient.withRateLimiter adds delay from retry-after headers diff --git a/.repos/effect-smol/.changeset/thirty-pans-love.md b/.repos/effect-smol/.changeset/thirty-pans-love.md new file mode 100644 index 00000000000..0d0a4f9c148 --- /dev/null +++ b/.repos/effect-smol/.changeset/thirty-pans-love.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +allow Model.Class for indexeddb schemas diff --git a/.repos/effect-smol/.changeset/three-corners-sort.md b/.repos/effect-smol/.changeset/three-corners-sort.md new file mode 100644 index 00000000000..ceb67c6a700 --- /dev/null +++ b/.repos/effect-smol/.changeset/three-corners-sort.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +port Url module from v3 diff --git a/.repos/effect-smol/.changeset/three-ravens-jam.md b/.repos/effect-smol/.changeset/three-ravens-jam.md new file mode 100644 index 00000000000..ba528ccbfe2 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/three-tomatoes-wave.md b/.repos/effect-smol/.changeset/three-tomatoes-wave.md new file mode 100644 index 00000000000..886f44fd49e --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tidy-icons-glow.md b/.repos/effect-smol/.changeset/tidy-icons-glow.md new file mode 100644 index 00000000000..dde79792333 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tidy-stars-drive.md b/.repos/effect-smol/.changeset/tidy-stars-drive.md new file mode 100644 index 00000000000..b31ea0808d5 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tiny-buckets-wave.md b/.repos/effect-smol/.changeset/tiny-buckets-wave.md new file mode 100644 index 00000000000..b01e82b8159 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tiny-lilies-flash.md b/.repos/effect-smol/.changeset/tiny-lilies-flash.md new file mode 100644 index 00000000000..f6368658898 --- /dev/null +++ b/.repos/effect-smol/.changeset/tiny-lilies-flash.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +fix idb entries transaction diff --git a/.repos/effect-smol/.changeset/tiny-rabbits-smile.md b/.repos/effect-smol/.changeset/tiny-rabbits-smile.md new file mode 100644 index 00000000000..9cc27e37800 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tocodecjson-return-json-type.md b/.repos/effect-smol/.changeset/tocodecjson-return-json-type.md new file mode 100644 index 00000000000..401d58c2c95 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/tool-get-json-schema-tests.md b/.repos/effect-smol/.changeset/tool-get-json-schema-tests.md new file mode 100644 index 00000000000..f94d0cf7761 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/true-actors-battle.md b/.repos/effect-smol/.changeset/true-actors-battle.md new file mode 100644 index 00000000000..b93e1858264 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/twenty-buttons-cheer.md b/.repos/effect-smol/.changeset/twenty-buttons-cheer.md new file mode 100644 index 00000000000..d1b709b46e1 --- /dev/null +++ b/.repos/effect-smol/.changeset/twenty-buttons-cheer.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +expose more atom Node properties diff --git a/.repos/effect-smol/.changeset/two-roses-double.md b/.repos/effect-smol/.changeset/two-roses-double.md new file mode 100644 index 00000000000..6efc5f52b05 --- /dev/null +++ b/.repos/effect-smol/.changeset/two-roses-double.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +improve openai websocket error status diff --git a/.repos/effect-smol/.changeset/upset-colts-stick.md b/.repos/effect-smol/.changeset/upset-colts-stick.md new file mode 100644 index 00000000000..5e87fdd32eb --- /dev/null +++ b/.repos/effect-smol/.changeset/upset-colts-stick.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add missing Equivalence.Date diff --git a/.repos/effect-smol/.changeset/vast-bananas-send.md b/.repos/effect-smol/.changeset/vast-bananas-send.md new file mode 100644 index 00000000000..815d683fbc0 --- /dev/null +++ b/.repos/effect-smol/.changeset/vast-bananas-send.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +clean up ShardId diff --git a/.repos/effect-smol/.changeset/vast-deserts-travel.md b/.repos/effect-smol/.changeset/vast-deserts-travel.md new file mode 100644 index 00000000000..262cd5463c9 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/violet-peaches-feel.md b/.repos/effect-smol/.changeset/violet-peaches-feel.md new file mode 100644 index 00000000000..b4e2ca243ee --- /dev/null +++ b/.repos/effect-smol/.changeset/violet-peaches-feel.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Effectable module diff --git a/.repos/effect-smol/.changeset/vitest-layer-top-level-options.md b/.repos/effect-smol/.changeset/vitest-layer-top-level-options.md new file mode 100644 index 00000000000..2ef279d0fc5 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/wacky-grapes-poke.md b/.repos/effect-smol/.changeset/wacky-grapes-poke.md new file mode 100644 index 00000000000..6e9284d8e9c --- /dev/null +++ b/.repos/effect-smol/.changeset/wacky-grapes-poke.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove superfluous error from SqlSchema.findAll signature diff --git a/.repos/effect-smol/.changeset/wacky-rice-add.md b/.repos/effect-smol/.changeset/wacky-rice-add.md new file mode 100644 index 00000000000..9de72c4df08 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/warm-friends-tie.md b/.repos/effect-smol/.changeset/warm-friends-tie.md new file mode 100644 index 00000000000..b600cb22663 --- /dev/null +++ b/.repos/effect-smol/.changeset/warm-friends-tie.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix cli subcommand context diff --git a/.repos/effect-smol/.changeset/warm-snails-shop.md b/.repos/effect-smol/.changeset/warm-snails-shop.md new file mode 100644 index 00000000000..907de8b5a97 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/wet-news-invent.md b/.repos/effect-smol/.changeset/wet-news-invent.md new file mode 100644 index 00000000000..f13ab2efb18 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/wild-readers-clean.md b/.repos/effect-smol/.changeset/wild-readers-clean.md new file mode 100644 index 00000000000..40b517e5054 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/wise-ants-wave.md b/.repos/effect-smol/.changeset/wise-ants-wave.md new file mode 100644 index 00000000000..e4966722f05 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/wise-flags-shift.md b/.repos/effect-smol/.changeset/wise-flags-shift.md new file mode 100644 index 00000000000..c470c58a677 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/wise-oranges-stay.md b/.repos/effect-smol/.changeset/wise-oranges-stay.md new file mode 100644 index 00000000000..263143b8c59 --- /dev/null +++ b/.repos/effect-smol/.changeset/wise-oranges-stay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow Context.Key to be covariant diff --git a/.repos/effect-smol/.changeset/witty-lobsters-share.md b/.repos/effect-smol/.changeset/witty-lobsters-share.md new file mode 100644 index 00000000000..c786da6867d --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/yellow-adults-study.md b/.repos/effect-smol/.changeset/yellow-adults-study.md new file mode 100644 index 00000000000..015ba9db526 --- /dev/null +++ b/.repos/effect-smol/.changeset/yellow-adults-study.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +openai ws tweaks diff --git a/.repos/effect-smol/.changeset/yellow-clocks-dance.md b/.repos/effect-smol/.changeset/yellow-clocks-dance.md new file mode 100644 index 00000000000..707dfbc5277 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/yellow-dingos-jump.md b/.repos/effect-smol/.changeset/yellow-dingos-jump.md new file mode 100644 index 00000000000..1a6af4ea8d8 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.changeset/young-doors-change.md b/.repos/effect-smol/.changeset/young-doors-change.md new file mode 100644 index 00000000000..c5ec50dbeb6 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.envrc b/.repos/effect-smol/.envrc new file mode 100644 index 00000000000..3550a30f2de --- /dev/null +++ b/.repos/effect-smol/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.repos/effect-smol/.github/actions/setup/action.yaml b/.repos/effect-smol/.github/actions/setup/action.yaml new file mode 100644 index 00000000000..94261b4ab90 --- /dev/null +++ b/.repos/effect-smol/.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: 25.9.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-smol/.github/workflows/ai-codegen.yml b/.repos/effect-smol/.github/workflows/ai-codegen.yml new file mode 100644 index 00000000000..47c58325e82 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.github/workflows/bundle-comment.yml b/.repos/effect-smol/.github/workflows/bundle-comment.yml new file mode 100644 index 00000000000..3ce7040e537 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.github/workflows/check.yml b/.repos/effect-smol/.github/workflows/check.yml new file mode 100644 index 00000000000..a16b645ad58 --- /dev/null +++ b/.repos/effect-smol/.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.8' + + 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.6.x + - 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:tsgo & + cd base && pnpm install && pnpm build:tsgo & + 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.6.x + - 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-smol/.github/workflows/release.yml b/.repos/effect-smol/.github/workflows/release.yml new file mode 100644 index 00000000000..de6a080b393 --- /dev/null +++ b/.repos/effect-smol/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Release +on: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +permissions: {} + +jobs: + release: + if: github.repository_owner == 'Effect-Ts' + name: Release + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + pull-requests: write + id-token: write + packages: 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@latest + - 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 }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.repos/effect-smol/.github/workflows/snapshot.yml b/.repos/effect-smol/.github/workflows/snapshot.yml new file mode 100644 index 00000000000..eb58a72565b --- /dev/null +++ b/.repos/effect-smol/.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:tsgo + - name: Create snapshot + id: snapshot + run: pnpx pkg-pr-new@0.0.62 publish --pnpm --comment=off ./packages/* ./packages/atom/* ./packages/ai/* ./packages/sql/* ./packages/tools/* diff --git a/.repos/effect-smol/.gitignore b/.repos/effect-smol/.gitignore new file mode 100644 index 00000000000..ecc1cb847c2 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.oxlintrc.json b/.repos/effect-smol/.oxlintrc.json new file mode 100644 index 00000000000..998e52ea76b --- /dev/null +++ b/.repos/effect-smol/.oxlintrc.json @@ -0,0 +1,25 @@ +{ + "$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/**/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-smol/.patterns/effect.md b/.repos/effect-smol/.patterns/effect.md new file mode 100644 index 00000000000..a778ae85c64 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.patterns/jsdoc.md b/.repos/effect-smol/.patterns/jsdoc.md new file mode 100644 index 00000000000..97066644e66 --- /dev/null +++ b/.repos/effect-smol/.patterns/jsdoc.md @@ -0,0 +1,80 @@ +# 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. Use `utils` only when no more specific shared or domain category fits; avoid `common` and do not use `misc`. + +## 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`, `converting`, `transforming`, `folding`, `splitting`, `concatenating` +- Encoding/data formats: `encoding`, `decoding`, `serialization` +- Observability: `tracing`, `metrics`, `logging` +- Other common concepts: `annotations`, `references`, `symbols`, `type IDs`, `configuration`, `math`, `comparisons`, `ordering`, `utils` + +## Alias Normalization + +Normalize high-confidence aliases, for example: + +- `Constructors` / `constructor` -> `constructors` +- `Layers` / `layer` -> `layers` +- `Models` / `Model` / `model` -> `models` +- `Combinators` -> `combinators` +- `Accessors` -> `accessors` +- `Guards` / `Guard` -> `guards` +- `Middleware` -> `middleware` +- `error` -> `errors` +- `Error Handling` -> `error handling` +- `Equivalence` -> `instances` +- `Scope` -> `resource management` +- `Wrapper` -> `wrapping` +- `Rate Limiting` -> `rate limiting` +- `Memory` -> `memory` +- `Config` -> `configuration` +- `Epoch` -> `constants` +- `Registry` -> `services` +- `Reflection` -> `reflection` +- `Modifiers` -> `modifiers` +- `Testing` -> `testing` +- `Token` -> `token` +- `Grouping` -> `grouping` +- `tranferables` -> `transferables` +- `Computer Use` -> `computer use` +- `Text Editor` -> `text editor` +- `Tool Search` -> `tool search` +- `Type IDs` / `type ids` -> `type IDs` +- `Services` / `Service` / `service` -> `services` +- `Re-exports` -> `re-exports` +- `protocol` -> `protocols` +- `Result` -> `results` +- `Boundaries` -> `boundaries` +- `Taking` -> `taking` +- `order` -> `ordering` +- `date & time` -> `DateTime` +- `serialization / deserialization` -> `serialization` +- `conversions` -> `converting` +- `transformations` -> `transforming` +- `Resource Management & Finalization` -> `resource management` +- `Run main` -> `running` +- `provider options` -> `configuration` +- `utilities` / `Utilities` -> `utils` + +## Distinctions + +Keep these distinctions: + +- `services` are service contracts/shapes, `tags` identify services in `Context`, 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-smol/.patterns/testing.md b/.repos/effect-smol/.patterns/testing.md new file mode 100644 index 00000000000..af6cc490d86 --- /dev/null +++ b/.repos/effect-smol/.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-smol/.specs/README.md b/.repos/effect-smol/.specs/README.md new file mode 100644 index 00000000000..573160801e9 --- /dev/null +++ b/.repos/effect-smol/.specs/README.md @@ -0,0 +1,4 @@ +# Specifications + +- [Effect SQL UniqueViolation SqlError Reason](./effect-sql-unique-violation.md) — Adds a `UniqueViolation` SQL error reason with a `constraint` property and updates driver classification for UNIQUE constraint violations. +- [Effect Platform Crypto Service](./effect-platform-crypto.md) — Adds a platform-agnostic `Crypto` service for cryptographic random bytes, native UUIDv4 generation, and digest operations. diff --git a/.repos/effect-smol/.specs/effect-platform-crypto.md b/.repos/effect-smol/.specs/effect-platform-crypto.md new file mode 100644 index 00000000000..04a7ba21839 --- /dev/null +++ b/.repos/effect-smol/.specs/effect-platform-crypto.md @@ -0,0 +1,338 @@ +# Effect Platform Crypto Service Specification + +## Summary + +Add a platform-agnostic `Crypto` service to `effect`. The service provides cryptographic random bytes, cryptographically secure random generators, UUIDv4 / UUIDv7 generation, and digest operations through an Effect service interface, with platform-specific implementations supplied by runtime packages such as `@effect/platform-node`, `@effect/platform-bun`, and `@effect/platform-browser`. + +The service replaces `Random.nextUUIDv4`. UUIDv4 and UUIDv7 generation are exposed on the `Crypto` service and derived by the `make` constructor from the primitive `randomBytes` implementation. UUIDv7 also uses the `Clock` service for the Unix millisecond timestamp. + +## Background and Research + +Platform services in this repository follow the pattern used by `FileSystem`: + +- Core service abstraction: `packages/effect/src/FileSystem.ts`. +- Node wrapper: `packages/platform-node/src/NodeFileSystem.ts`. +- Bun wrapper: `packages/platform-bun/src/BunFileSystem.ts`. +- Shared Node/Bun implementation: `packages/platform-node-shared/src/NodeFileSystem.ts`. + +Important conventions from that implementation: + +- The service interface lives in `effect` and carries a unique `TypeId` property. +- The service tag is created with `Context.Service`. +- Platform-specific packages expose `layer` values that provide the service. +- Node and Bun can share implementation code when Bun supports the relevant Node API. +- Generated `index.ts` barrel files must not be edited manually; run `pnpm codegen` after adding modules. +- Platform errors should use `PlatformError` where possible. +- Library implementation code should avoid `async` / `await` and `try` / `catch`, using Effect APIs instead. + +The current base `Random` service is not cryptographically secure because its default implementation is based on `Math.random`. `Crypto` extends the `Random` service shape so platform crypto implementations can expose cryptographically secure random number generation through `Crypto` service methods. + +## User Requirements and Clarifications + +- Add a platform-agnostic `Crypto` Effect service. +- `Crypto` must extend the `Random` service. +- Include UUIDv4 and UUIDv7 generators so users can stop using `Random.nextUUIDv4`. +- `randomUUIDv4` must be a service method derived by `make`, not a primitive required from platform implementations. +- `randomUUIDv4` must format bytes from the service's `randomBytes(16)` and follow UUIDv4 version/variant requirements. +- `randomUUIDv7` must be a service method derived by `make`, format bytes from `randomBytes(16)`, use `Clock.currentTimeMillis` for the 48-bit Unix millisecond timestamp, and follow UUIDv7 version/variant requirements. +- Define `DigestAlgorithm` as a string literal union. +- Add cryptographically secure counterparts to the `Random` module generators as service methods with clearer names: `random`, `randomBoolean`, `randomInt`, `randomBetween`, `randomIntBetween`, `randomShuffle`, `randomUUIDv4`, and `randomUUIDv7`. +- Keep the initial `Crypto` surface limited to random bytes, random generators, UUID generation, and digests. + +## Proposed Public API + +Add `packages/effect/src/Crypto.ts`. + +```ts +import * as Context from "./Context.ts" +import * as Clock from "./Clock.ts" +import type * as Effect from "./Effect.ts" +import type { PlatformError } from "./PlatformError.ts" +import type * as Random from "./Random.ts" + +const TypeId = "~effect/platform/Crypto" + +export type DigestAlgorithm = "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512" + +export interface Crypto extends Random.Random { + readonly [TypeId]: typeof TypeId + + readonly randomBytes: ( + size: number + ) => Effect.Effect + + readonly digest: ( + algorithm: DigestAlgorithm, + data: Uint8Array + ) => Effect.Effect + + readonly random: Effect.Effect + readonly randomBoolean: Effect.Effect + readonly randomInt: Effect.Effect + readonly randomBetween: (min: number, max: number) => Effect.Effect + readonly randomIntBetween: ( + min: number, + max: number, + options?: { readonly halfOpen?: boolean | undefined } + ) => Effect.Effect + readonly randomShuffle: (elements: Iterable) => Effect.Effect> + readonly randomUUIDv4: Effect.Effect + readonly randomUUIDv7: Effect.Effect +} + +export const Crypto: Context.Service = Context.Service("effect/platform/Crypto") + +export const make: (impl: Omit) => Crypto +``` + +## Functional Requirements + +### Core `Crypto` Module + +1. Add `packages/effect/src/Crypto.ts`. +2. Define `TypeId` as `"~effect/platform/Crypto"`. +3. Define `DigestAlgorithm` as a top-level string literal union. +4. Use Web Crypto algorithm names `"SHA-1"`, `"SHA-256"`, `"SHA-384"`, and `"SHA-512"`. +5. Do not require platform implementations to provide derived random generator helpers. +6. Define `Crypto` as an extension of `Random.Random` with `randomBytes` and `digest`. +7. Include `randomUUIDv4`, `randomUUIDv7`, and the random generator helpers on the `Crypto` service interface. +8. Define the service tag with `Context.Service("effect/platform/Crypto")`. +9. Do not add top-level accessors; users should retrieve the service and call its methods. +10. Add cryptographically secure random generator helpers matching the `Random` module capabilities with clearer names to the service interface. +11. Derive service `randomUUIDv4` from `randomBytes(16)`. +12. Derive service `randomUUIDv7` from `Clock.currentTimeMillis` and `randomBytes(16)`. +13. Keep the core module platform-agnostic; it must not import `node:crypto` or rely directly on `globalThis.crypto`. +14. The `make` helper should accept the primitive implementation and derive random helper methods, similar to `ChildProcessSpawner.make`. + +### Random Generator Requirements + +1. `random` must use `Crypto.nextDoubleUnsafe`. +2. `randomBoolean` must use `Crypto.nextDoubleUnsafe() > 0.5`. +3. `randomInt` must use `Crypto.nextIntUnsafe`. +4. `randomBetween` must follow `Random.nextBetween` semantics. +5. `randomIntBetween` must follow `Random.nextIntBetween` semantics, including `halfOpen`. +6. `randomShuffle` must follow `Random.shuffle` semantics. +7. The random generator helper names must be more descriptive than the base `Random` names. + +### UUIDv4 Requirements + +1. `randomUUIDv4` must return a lowercase UUIDv4 string. +2. It must satisfy the standard UUIDv4 shape: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`, where `y` is one of `8`, `9`, `a`, or `b`. +3. It must call `randomBytes(16)` and format those bytes according to UUIDv4 rules. +4. It must set the version bits to `0100` in byte 6. +5. It must set the variant bits to `10` in byte 8. +6. It must not use `Random`, `Math.random`, `Date.now`, `new Date`, or platform `crypto.randomUUID`. + +### UUIDv7 Requirements + +1. `randomUUIDv7` must return a lowercase UUIDv7 string. +2. It must satisfy the standard UUIDv7 shape: `xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx`, where `y` is one of `8`, `9`, `a`, or `b`. +3. Bytes 0 through 5 must encode the current Unix timestamp in milliseconds from `Clock.currentTimeMillis` as a big-endian 48-bit integer. +4. Remaining UUID bits must come from `randomBytes(16)` except for overwritten timestamp, version, and variant bits. +5. It must set the version bits to `0111` in byte 6. +6. It must set the variant bits to `10` in byte 8. +7. It must not use `Random`, `Math.random`, `Date.now`, `new Date`, or platform `crypto.randomUUID`. + +### Random Bytes Requirements + +1. `randomBytes(size)` must generate cryptographically secure random bytes. +2. `size` must be a safe non-negative integer. +3. Invalid sizes must fail with `PlatformError.badArgument`. +4. `randomBytes(0)` should succeed with an empty `Uint8Array`. +5. Browser implementations must respect the `crypto.getRandomValues` per-call limit by chunking large requests, typically at `65_536` bytes. +6. Returned byte arrays should be fresh arrays owned by the caller. + +### Digest Requirements + +1. `digest(algorithm, data)` must compute a cryptographic digest of `data`. +2. The returned value must be a `Uint8Array`. +3. The input `data` must not be mutated. +4. Platform failures must be represented as `PlatformError` values. +5. SHA-1 is included for compatibility with Web Crypto and existing protocols, but documentation must state that SHA-1 should not be used for new security-sensitive designs. + +## Platform Implementation Plan + +### Node and Bun Shared Implementation + +Add `packages/platform-node-shared/src/NodeCrypto.ts`. + +Requirements: + +1. Import `node:crypto`. +2. Implement `randomBytes` with `crypto.randomBytes`. +3. Implement `nextIntUnsafe` and `nextDoubleUnsafe` with synchronous cryptographically secure random bytes. +4. Implement `digest` with `crypto.webcrypto.subtle.digest` or equivalent Node crypto APIs. +5. Convert backend exceptions and promise rejections to `PlatformError`. +6. Export `layer: Layer.Layer`. + +Add thin runtime wrappers: + +- `packages/platform-node/src/NodeCrypto.ts` re-exports `NodeCrypto.layer`. +- `packages/platform-bun/src/BunCrypto.ts` re-exports the shared layer if Bun compatibility is sufficient. + +### Browser Implementation + +Add `packages/platform-browser/src/BrowserCrypto.ts`. + +Requirements: + +1. Use `globalThis.crypto` as the backend. +2. Implement `randomBytes` with `crypto.getRandomValues`, chunking large requests. +3. Implement `nextIntUnsafe` and `nextDoubleUnsafe` with synchronous `crypto.getRandomValues`. +4. Implement `digest` with `crypto.subtle.digest`. +5. Fail with a clear `PlatformError.systemError({ _tag: "Unknown" })` when required crypto capabilities are unavailable. +6. Do not require Node types or Node imports. + +## Service Aggregation + +Update Node service aggregation: + +- Add `Crypto` to `packages/platform-node/src/NodeServices.ts` imports and `NodeServices` union. +- Merge `NodeCrypto.layer` into `NodeServices.layer`. + +Update Bun service aggregation: + +- Add `Crypto` to `packages/platform-bun/src/BunServices.ts` imports and `BunServices` union. +- Merge `BunCrypto.layer` into `BunServices.layer`. + +No browser aggregate service module currently exists. Do not introduce one solely for this task unless maintainers request it. + +## Random Migration Plan + +1. Remove `Random.nextUUIDv4`. +2. Update `Random` module documentation to remove UUID examples. +3. Update `packages/effect/test/Random.test.ts` to remove UUID-specific tests. +4. Add tests and documentation showing `Crypto.Crypto` service `randomUUIDv4` as the replacement. +5. Document that the base `Random` service is not cryptographically secure and should not be used for security-sensitive values. + +## Documentation Requirements + +Add detailed JSDoc to `packages/effect/src/Crypto.ts` and update `packages/effect/src/Random.ts`. + +The documentation must explain: + +1. `Crypto` is for cryptographic randomness and cryptographic operations. +2. The base `Random` service is not cryptographically secure. +3. `Random.withSeed` provides deterministic replacement for repeatability; predictable seeds are not cryptographically secure. +4. UUID generation should use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, not `Random.nextUUIDv4`. +5. Platform implementations must be provided through layers. +6. SHA-1 is available only for compatibility and should be avoided for new security-sensitive designs. + +## Testing Requirements + +### Core Tests + +Add `packages/effect/test/Crypto.test.ts`. + +Test cases: + +1. `DigestAlgorithm` supports the expected string literal values. +2. `randomBytes` delegates to the provided service. +3. Random generator methods derive from the `Random` methods on the provided `Crypto` service. +4. `randomUUIDv4` formats bytes from the provided service's `randomBytes` method. +5. `randomUUIDv7` formats bytes from the provided service's `randomBytes` method and the `Clock` timestamp. +6. `digest` delegates to the service. +7. A custom `Crypto` service can be provided via `Effect.provideService`. + +### Node Tests + +Add `packages/platform-node/test/NodeCrypto.test.ts`. + +Test cases: + +1. `randomBytes(0)` returns an empty `Uint8Array`. +2. `randomBytes(32)` returns 32 bytes. +3. Invalid sizes fail with `PlatformError`. +4. Service `randomUUIDv4` returns a valid UUIDv4 string when provided with the Node layer. +5. Service `randomUUIDv7` returns a valid UUIDv7 string with the current `Clock` timestamp when provided with the Node layer. +6. Two UUIDs generated from Node cryptographic random bytes are not equal in a basic smoke test. +7. SHA-256 digest of a known input matches the known vector. + +### Browser Tests + +Add `packages/platform-browser/test/BrowserCrypto.test.ts`. + +Test cases: + +1. `randomBytes` delegates to `getRandomValues` and handles chunking. +2. Service `randomUUIDv4` formats bytes from browser `getRandomValues`. +3. Service `randomUUIDv7` formats bytes from browser `getRandomValues` and the `Clock` timestamp. +4. Missing crypto capabilities fail with `PlatformError`. +5. SHA-256 digest matches a known vector if `crypto.subtle` is available. + +## Generated Files + +After adding modules, run `pnpm codegen`. + +Expected generated barrel updates: + +- `packages/effect/src/index.ts`. +- `packages/platform-node/src/index.ts`. +- `packages/platform-bun/src/index.ts`. +- `packages/platform-browser/src/index.ts`. + +Do not manually edit generated barrel files. + +## Changeset Requirements + +Add a changeset covering at least: + +- `effect`. +- `@effect/platform-node`. +- `@effect/platform-node-shared`. +- `@effect/platform-bun`. +- `@effect/platform-browser`. + +The changeset must state: + +- A new platform-agnostic `Crypto` service was added. +- `Crypto` extends `Random` and exposes cryptographically secure random generator helpers. +- `Crypto` service `randomUUIDv4` formats bytes from the platform `Crypto` service. +- `Crypto` service `randomUUIDv7` formats bytes from the platform `Crypto` service and uses the `Clock` service timestamp. +- `DigestAlgorithm` is represented as a string literal union. +- Users should migrate away from `Random.nextUUIDv4` for UUID generation. + +## Validation Plan + +Run validation in this order: + +1. `pnpm lint-fix`. +2. `pnpm codegen`. +3. `pnpm test packages/effect/test/Crypto.test.ts`. +4. `pnpm test packages/platform-node/test/NodeCrypto.test.ts`. +5. `pnpm test packages/platform-browser/test/BrowserCrypto.test.ts`. +6. `pnpm test packages/effect/test/Random.test.ts`. +7. `pnpm check:tsgo`. +8. If `pnpm check:tsgo` repeatedly fails due to stale caches, run `pnpm clean` and rerun `pnpm check:tsgo`. +9. For localized `effect` package documentation changes, run `cd packages/effect && pnpm docgen`. + +## Acceptance Criteria + +1. `Crypto` is available from `effect/Crypto`. +2. `DigestAlgorithm` is a top-level string literal union. +3. `Crypto` extends the base `Random` service type. +4. Service `randomUUIDv4` derives UUIDs from service `randomBytes(16)` and formats UUIDv4 version/variant bits correctly. +5. The service interface contains `randomUUIDv4` and `randomUUIDv7` as derived methods. +6. `randomBytes` validates sizes and returns cryptographically secure random bytes. +7. `digest` supports SHA-1, SHA-256, SHA-384, and SHA-512 through `DigestAlgorithm` values. +8. The `Crypto` service exposes `random`, `randomBoolean`, `randomInt`, `randomBetween`, `randomIntBetween`, `randomShuffle`, `randomUUIDv4`, and `randomUUIDv7`. +9. Platform failures are represented as `PlatformError` values. +10. Core and platform tests pass. +11. JSDoc examples compile with docgen. +12. Generated barrel files are regenerated with `pnpm codegen`. +13. A changeset documents the new service and UUID migration guidance. + +## Risks and Mitigations + +1. Risk: Formatting UUIDv4 in the core module could set version or variant bits incorrectly. + - Mitigation: Add tests with deterministic bytes and validate the UUIDv4 shape. +2. Risk: Browser `getRandomValues` is unavailable in some environments. + - Mitigation: Fail with a structured `PlatformError` for effectful operations. +3. Risk: `DigestAlgorithm` string inputs invite typos or inconsistent algorithm spelling. + - Mitigation: Use a top-level string literal union with Web Crypto algorithm names and centralize platform mapping. +4. Risk: Existing users rely on deterministic UUIDs from `Random.nextUUIDv4` in tests. + - Mitigation: Document that deterministic IDs should be provided through a fake `Crypto` service or an explicit test service. + +## Open Questions + +None. The user clarified that `Random.nextUUIDv4` should be removed and the initial crypto surface should remain limited. PR review clarified that `DigestAlgorithm` should be a string literal union and that random helpers should live on the service interface. diff --git a/.repos/effect-smol/.specs/effect-sql-unique-violation.md b/.repos/effect-smol/.specs/effect-sql-unique-violation.md new file mode 100644 index 00000000000..b48c13b00cb --- /dev/null +++ b/.repos/effect-smol/.specs/effect-sql-unique-violation.md @@ -0,0 +1,497 @@ +# Effect SQL UniqueViolation SqlError Reason Specification + +## Summary + +Add a new SQL error reason class named `UniqueViolation` to the Effect SQL error model. The reason must be used when a database driver reports a UNIQUE constraint violation, and it must expose a required `constraint: string` property that identifies the violated constraint or unique index when possible. If the identifier cannot be determined reliably, `constraint` must be `"unknown"`. + +## Background and Research + +Current SQL error classification is centered in `packages/effect/src/unstable/sql/SqlError.ts` and in driver-specific clients: + +- Core reason classes and `SqlErrorReason` union: `packages/effect/src/unstable/sql/SqlError.ts`. +- SQLite shared classifier: `classifySqliteError` in `packages/effect/src/unstable/sql/SqlError.ts`. +- PostgreSQL classifier: `packages/sql/pg/src/PgClient.ts`. +- PGlite classifier: `packages/sql/pglite/src/PgliteClient.ts`. +- MySQL classifier: `packages/sql/mysql2/src/MysqlClient.ts`. +- MSSQL classifier: `packages/sql/mssql/src/MssqlClient.ts`. + +Today, all integrity constraint violations are represented by `ConstraintError`. This includes database-reported UNIQUE violations such as PostgreSQL/PGlite SQLSTATE `23505`, SQLite `SQLITE_CONSTRAINT_UNIQUE` / extended result code `2067`, MySQL duplicate-entry errno `1062`, and MSSQL errors `2601` / `2627`. The new `UniqueViolation` reason should represent those unique-specific cases, while non-unique integrity violations continue to use `ConstraintError`. + +Existing consumers also key off `ConstraintError` for concurrency/duplicate-insert behavior: + +- `packages/effect/src/unstable/sql/Migrator.ts` maps migration insert `ConstraintError` to a locked migration error. +- `packages/effect/src/unstable/eventlog/SqlEventLogServerUnencrypted.ts` ignores a duplicate singleton insert by catching `ConstraintError`. + +Those consumers must be updated to treat `UniqueViolation` the same way where appropriate, otherwise the new classification would break existing behavior. + +Existing tests to extend include: + +- `packages/effect/test/unstable/sql/SqlError.test.ts`. +- `packages/sql/pg/test/SqlErrorClassification.test.ts`. +- `packages/sql/mysql2/test/SqlErrorClassification.test.ts`. +- `packages/sql/mssql/test/SqlErrorClassification.test.ts`. + +A new PGlite classification test is expected because `packages/sql/pglite/test` currently has no dedicated `SqlErrorClassification.test.ts`. + +## User Requirements and Clarifications + +- Add a new `SqlError` reason class named `UniqueViolation`. +- Use `UniqueViolation` when a UNIQUE constraint is violated. +- `UniqueViolation` must include a `constraint` property. +- Map all supported drivers that can specifically identify a unique constraint violation. +- Keep other constraint violations mapped to `ConstraintError`. +- If a constraint identifier cannot be determined, set `constraint` to `"unknown"`. + +## Functional Requirements + +### Core SQL Error Model + +1. Add `UniqueViolation` in `packages/effect/src/unstable/sql/SqlError.ts` as a sibling reason class to `ConstraintError`. +2. `UniqueViolation` must: + - Have tag `"UniqueViolation"`. + - Have class identifier `"effect/sql/SqlError/UniqueViolation"`. + - Include all common reason fields: `cause`, optional `message`, optional `operation`. + - Include required `constraint: string`. + - Set `readonly [ReasonTypeId] = ReasonTypeId`. + - Return `false` from `isRetryable`. +3. Add `UniqueViolation` to: + - The `SqlErrorReason` TypeScript union. + - The `SqlErrorReason` `Schema.Union` type tuple. + - The runtime `Schema.Union([...])` list. +4. Place `UniqueViolation` adjacent to `ConstraintError`, preferably before `ConstraintError`, to keep unique violations grouped with integrity constraint reasons and to avoid any accidental preference for the broader type in future schema changes. +5. Existing `SqlError.message`, `SqlError.cause`, `SqlError.isRetryable`, `isSqlError`, and `isSqlErrorReason` behavior must remain unchanged. + +### Constraint Identifier Normalization + +1. `UniqueViolation.constraint` should be the best available database-reported identifier. +2. The fallback value must be exactly `"unknown"`. +3. All extraction helpers must normalize identifiers consistently: + - Accept only strings. + - Trim leading and trailing whitespace. + - If the trimmed string is empty, use `"unknown"`. +4. Extraction precedence must be: + - Prefer explicit structured fields such as `cause.constraint` when available. + - Use driver-specific message parsing only for common stable messages when no structured field exists. + - Never throw during extraction; malformed or absent metadata must produce `"unknown"`. +5. The `constraint` property may represent a constraint name, unique index name, key name, or stable database-produced descriptor depending on the driver. + +### Driver Classification Requirements + +#### PostgreSQL (`@effect/sql-pg`) + +1. SQLSTATE `23505` must map to `UniqueViolation` before the broader `code.startsWith("23")` `ConstraintError` branch. +2. `constraint` must come from normalized `cause.constraint` when it is a non-empty string. +3. If `cause.constraint` is unavailable, not a string, or blank, use `"unknown"`. +4. Other `23***` SQLSTATE values, such as `23503`, must continue to map to `ConstraintError`. + +#### PGlite (`@effect/sql-pglite`) + +1. SQLSTATE `23505` must map to `UniqueViolation` before the broader `code.startsWith("23")` `ConstraintError` branch. +2. `constraint` must come from normalized `cause.constraint` when it is a non-empty string. +3. If unavailable or blank, use `"unknown"`. +4. Other `23***` SQLSTATE values must continue to map to `ConstraintError`. + +#### SQLite-family drivers + +Applies to all users of shared `classifySqliteError`, including sqlite-node, sqlite-bun, sqlite-wasm, sqlite-react-native, sqlite-do, libsql, and OPFS worker call sites. + +1. String code `SQLITE_CONSTRAINT_UNIQUE` must map to `UniqueViolation`. +2. Numeric extended result code `2067` must map to `UniqueViolation`. +3. Generic `SQLITE_CONSTRAINT` and base numeric result code `19` must continue to map to `ConstraintError` unless the unique extended code is known. +4. SQLite primary-key-specific codes such as `SQLITE_CONSTRAINT_PRIMARYKEY` / `1555` are out of scope for this request and should remain classified as they are today unless separately requested. +5. `constraint` extraction must: + - Prefer normalized `cause.constraint` if present. + - Otherwise parse common messages such as `UNIQUE constraint failed: table.column` and use the normalized suffix after `: `, for example `table.column`. + - Otherwise use `"unknown"`. + +#### MySQL (`@effect/sql-mysql2`) + +1. Errno `1062` must map to `UniqueViolation` before the broader constraint error code set. +2. Other MySQL constraint errno values, including `1022`, `1048`, `1169`, `1216`, `1217`, `1451`, `1452`, and `1557`, must continue to map to `ConstraintError`. +3. `constraint` extraction must: + - Prefer normalized `cause.constraint` if present. + - Otherwise parse common duplicate-entry messages from `cause.sqlMessage` or `cause.message` containing `for key 'key_name'`, `for key `key_name``, or `for key key_name`. + - Otherwise use `"unknown"`. +4. If errno `1062` represents a duplicate primary key, the key name (for example `PRIMARY`) may be used as the `constraint` because MySQL reports primary-key and unique-index duplicates through the same duplicate-entry code. + +#### MSSQL (`@effect/sql-mssql`) + +1. Error numbers `2601` and `2627` must map to `UniqueViolation` before the broader constraint error code set. +2. Other MSSQL constraint errors, including `515` and `547`, must continue to map to `ConstraintError`. +3. `constraint` extraction must: + - Prefer normalized `cause.constraint` if present. + - For `2627`, parse common messages containing `constraint 'constraint_name'`. + - For `2601`, parse common messages containing `unique index 'index_name'`. + - Otherwise use `"unknown"`. + +### Existing Consumer Requirements + +1. `packages/effect/src/unstable/sql/Migrator.ts` must treat `UniqueViolation` the same way it currently treats `ConstraintError` when mapping duplicate migration insert failures to `MigrationError({ kind: "Locked", message: "Migrations already running" })`. +2. `packages/effect/src/unstable/eventlog/SqlEventLogServerUnencrypted.ts` must catch `UniqueViolation` in the same path that currently catches `ConstraintError` for duplicate singleton `remote_id` insertion. +3. The implementation should avoid duplicating tag checks across call sites if a small local predicate improves readability, but no new public helper is required by this request. + +### Backward Compatibility and Release Notes + +1. This is an additive reason class, but it changes classification for unique constraint violations from `ConstraintError` to `UniqueViolation`. +2. Code that only handles generic `ConstraintError` will no longer match unique violations by tag. The changeset must explicitly mention this behavior change. +3. Existing `ConstraintError` remains unchanged for non-unique constraint violations. +4. Existing uses that check `error.reason.isRetryable` will preserve behavior because both `ConstraintError` and `UniqueViolation` are non-retryable. +5. Existing schema encoding/decoding for previous reason classes must remain unchanged. +6. The changeset should be a patch release entry unless maintainers decide the tag-level classification change needs a larger semver impact. + +## Non-Functional Requirements + +1. Follow existing code style and patterns in `.patterns/` and surrounding source files. +2. Do not manually edit generated barrel `index.ts` files. No new module is expected, so `pnpm codegen` should not be required. +3. Do not use `async` / `await` or `try` / `catch` in Effect library implementation code. +4. Keep helper functions small and pure. +5. Avoid broad or fragile message parsing; use simple, well-scoped extraction and fall back to `"unknown"`. + +## Acceptance Criteria + +1. `new UniqueViolation({ cause, message, operation, constraint })` can be constructed and is recognized by `isSqlErrorReason`. +2. `UniqueViolation` has `_tag === "UniqueViolation"`, `isRetryable === false`, and exposes the normalized `constraint` string supplied to the constructor/classifier. +3. `SqlErrorReason` schema can encode/decode `SqlError` values wrapping `UniqueViolation`. +4. PostgreSQL and PGlite SQLSTATE `23505` classify as `UniqueViolation`, using non-empty `cause.constraint` when available and `"unknown"` otherwise. +5. PostgreSQL and PGlite non-unique `23***` SQLSTATE values still classify as `ConstraintError`. +6. SQLite `SQLITE_CONSTRAINT_UNIQUE` and numeric extended code `2067` classify as `UniqueViolation`. +7. SQLite generic constraint code `SQLITE_CONSTRAINT` and base numeric code `19` still classify as `ConstraintError`. +8. MySQL errno `1062` classifies as `UniqueViolation`; other existing constraint errno values still classify as `ConstraintError`. +9. MSSQL numbers `2601` and `2627` classify as `UniqueViolation`; other existing constraint numbers still classify as `ConstraintError`. +10. `Migrator.ts` and `SqlEventLogServerUnencrypted.ts` preserve their existing duplicate-insert behavior after unique violations classify as `UniqueViolation`. +11. All new and existing affected tests pass. +12. Type checking, linting, and localized docgen checks pass as required by repository guidelines. +13. A changeset is added documenting the new `UniqueViolation` reason and the classification change for unique constraint violations. + +## Implementation Plan + +### Task 1: Add the core `UniqueViolation` reason class and core tests + +Status: Completed. The core `UniqueViolation` reason was added to `SqlError.ts`, included in the `SqlErrorReason` type and schema unions, and covered by construction, recognition, retryability, constraint-retention, and schema roundtrip tests in `SqlError.test.ts`. + +Validation completed for this task: + +- `pnpm lint-fix` +- `pnpm test packages/effect/test/unstable/sql/SqlError.test.ts` +- `pnpm check:tsgo` +- `cd packages/effect && pnpm docgen` + +Scope: + +- `packages/effect/src/unstable/sql/SqlError.ts`. +- `packages/effect/test/unstable/sql/SqlError.test.ts`. + +Steps: + +1. Define `UniqueViolation` fields by extending the existing reason fields with `constraint: Schema.String`. +2. Add `export class UniqueViolation extends Schema.TaggedErrorClass("effect/sql/SqlError/UniqueViolation")("UniqueViolation", UniqueViolationFields)`. +3. Implement `readonly [ReasonTypeId] = ReasonTypeId` and `get isRetryable(): boolean { return false }`. +4. Add `UniqueViolation` to the `SqlErrorReason` union and schema union, adjacent to and preferably before `ConstraintError`. +5. Update `packages/effect/test/unstable/sql/SqlError.test.ts`: + - Include `UniqueViolation` in reason coverage using construction data that includes `constraint`. + - Add a focused assertion that `constraint` is retained. + - Add schema roundtrip coverage for `UniqueViolation`. +6. Run validation for this task: + - `pnpm lint-fix` + - `pnpm test packages/effect/test/unstable/sql/SqlError.test.ts` + - `pnpm check:tsgo` + - `cd packages/effect && pnpm docgen` + +Rationale: + +- The public reason class, union, schema, and tests must land together to keep the package type-correct and validated. + +### Task 2: Update core consumers that currently catch `ConstraintError` + +Status: Completed. Local `isConstraintConflict` predicates were added in both core consumers so `ConstraintError` and `UniqueViolation` continue to follow the existing duplicate/locked paths while non-SQL errors and other SQL errors remain unchanged. + +Validation completed for this task: + +- `pnpm lint-fix` +- `pnpm test packages/effect/test/unstable/sql/SqlError.test.ts` +- `pnpm test packages/sql/pg/test/SqlEventLogServerUnencrypted.test.ts` +- `pnpm test packages/sql/mysql2/test/SqlEventLogServerUnencrypted.test.ts` +- `pnpm test packages/sql/sqlite-node/test/SqlEventLogServerUnencrypted.test.ts` +- `pnpm check:tsgo` +- `cd packages/effect && pnpm docgen` + +Scope: + +- `packages/effect/src/unstable/sql/Migrator.ts`. +- `packages/effect/src/unstable/eventlog/SqlEventLogServerUnencrypted.ts`. +- Existing integration tests that exercise those paths. + +Steps: + +1. Update the migration insert error mapping so `error.reason._tag === "UniqueViolation"` is handled the same as `"ConstraintError"`. +2. Update the event log remote-id singleton insert catch predicate so `UniqueViolation` is handled the same as `ConstraintError`. +3. Prefer small local predicates such as `isConstraintConflict` if they make the affected code clearer. +4. Run validation for this task: + - `pnpm lint-fix` + - `pnpm test packages/effect/test/unstable/sql/SqlError.test.ts` + - `pnpm test packages/sql/pg/test/SqlEventLogServerUnencrypted.test.ts` + - `pnpm test packages/sql/mysql2/test/SqlEventLogServerUnencrypted.test.ts` + - `pnpm test packages/sql/sqlite-node/test/SqlEventLogServerUnencrypted.test.ts` + - `pnpm check:tsgo` + - `cd packages/effect && pnpm docgen` + +Rationale: + +- Once unique violations stop being `ConstraintError`, these existing consumers would otherwise regress. Updating them is required for a shippable core behavior change. + +### Task 3: Update SQLite shared classification and tests + +Status: Completed. The shared SQLite classifier now normalizes unique constraint identifiers, extracts SQLite unique descriptors from explicit `cause.constraint` values or common `UNIQUE constraint failed: ...` messages, and maps both `SQLITE_CONSTRAINT_UNIQUE` and numeric extended result code `2067` to `UniqueViolation` before generic constraint handling. Generic `SQLITE_CONSTRAINT`, base numeric result code `19`, and primary-key-specific SQLite constraint codes remain classified as `ConstraintError`. + +Validation completed for this task: + +- `pnpm lint-fix` +- `pnpm test packages/effect/test/unstable/sql/SqlError.test.ts` +- `pnpm check:tsgo` +- `cd packages/effect && pnpm docgen` + +Scope: + +- `packages/effect/src/unstable/sql/SqlError.ts`. +- `packages/effect/test/unstable/sql/SqlError.test.ts`. + +Steps: + +1. Add small pure helpers in `SqlError.ts` to safely extract normalized string properties and SQLite unique constraint descriptors. +2. In string-code classification, check exact `SQLITE_CONSTRAINT_UNIQUE` before generic `SQLITE_CONSTRAINT`. +3. In numeric-code classification, check exact extended result code `2067` before masking to the base result code. +4. Create `UniqueViolation` with extracted `constraint` or `"unknown"`. +5. Keep generic `SQLITE_CONSTRAINT` and base result code `19` mapped to `ConstraintError`. +6. Do not add special mapping for SQLite primary-key-specific codes in this task. +7. Add tests for: + - String code `SQLITE_CONSTRAINT_UNIQUE` -> `UniqueViolation`. + - Numeric code / errno `2067` -> `UniqueViolation`. + - Constraint descriptor parsing from `UNIQUE constraint failed: users.email`. + - Blank or missing identifiers fall back to `"unknown"`. + - Generic constraint remains `ConstraintError`. +8. Run validation for this task: + - `pnpm lint-fix` + - `pnpm test packages/effect/test/unstable/sql/SqlError.test.ts` + - `pnpm check:tsgo` + - `cd packages/effect && pnpm docgen` + +Rationale: + +- SQLite classification lives in the core Effect package, so pairing it with the core SqlError tests keeps validation self-contained. + +### Task 4: Update PostgreSQL classification and tests + +Status: Completed. PostgreSQL SQLSTATE `23505` is now classified as `UniqueViolation` before the broader integrity-constraint branch. The classifier normalizes `cause.constraint` by accepting only strings, trimming whitespace, and falling back to `"unknown"` for missing, non-string, or blank values. Non-unique integrity SQLSTATEs such as `23503` remain classified as `ConstraintError`. + +Validation completed for this task: + +- `pnpm lint-fix` +- `pnpm test packages/sql/pg/test/SqlErrorClassification.test.ts` +- `pnpm check:tsgo` + +Scope: + +- `packages/sql/pg/src/PgClient.ts`. +- `packages/sql/pg/test/SqlErrorClassification.test.ts`. + +Steps: + +1. Import `UniqueViolation` from `effect/unstable/sql/SqlError`. +2. Add a small helper to extract normalized `constraint` from a cause object, falling back to `"unknown"`. +3. Check `code === "23505"` before `code.startsWith("23")`. +4. Return `new UniqueViolation({ ...props, constraint })` for `23505`. +5. Ensure non-unique SQLSTATEs such as `23503` still return `ConstraintError`. +6. Extend `packages/sql/pg/test/SqlErrorClassification.test.ts` to assert: + - `23505` -> `UniqueViolation`. + - Constraint field is preserved from `cause.constraint` after trimming. + - Missing or blank constraint falls back to `"unknown"`. + - `23503` remains `ConstraintError`. +7. Run validation for this task: + - `pnpm lint-fix` + - `pnpm test packages/sql/pg/test/SqlErrorClassification.test.ts` + - `pnpm check:tsgo` + +Rationale: + +- PostgreSQL has its own package and test surface; keeping it separate makes the task independently shippable. + +### Task 5: Update PGlite classification and tests + +Status: Completed. PGlite SQLSTATE `23505` is now classified as `UniqueViolation` before the broader integrity-constraint branch. The classifier normalizes `cause.constraint` by accepting only strings, trimming whitespace, and falling back to `"unknown"` for missing, non-string, or blank values. Non-unique integrity SQLSTATEs such as `23503` remain classified as `ConstraintError`. A dedicated `SqlErrorClassification.test.ts` uses a lightweight rejecting PGlite client mock to exercise classification without a live database. + +Validation completed for this task: + +- `pnpm lint-fix` +- `pnpm test packages/sql/pglite/test/SqlErrorClassification.test.ts` +- `pnpm check:tsgo` + +Scope: + +- `packages/sql/pglite/src/PgliteClient.ts`. +- New `packages/sql/pglite/test/SqlErrorClassification.test.ts`. + +Steps: + +1. Import `UniqueViolation` from `effect/unstable/sql/SqlError`. +2. Add a small helper to extract normalized `constraint` from a cause object, falling back to `"unknown"`. +3. Check `code === "23505"` before `code.startsWith("23")`. +4. Return `new UniqueViolation({ ...props, constraint })` for `23505`. +5. Ensure non-unique SQLSTATEs such as `23503` still return `ConstraintError`. +6. Add a dedicated classification test following nearby `@effect/vitest` patterns. Prefer a lightweight mocked/rejecting client path that exercises the PGlite classifier without needing a live database. +7. Test: + - `23505` -> `UniqueViolation`. + - Constraint field is preserved from `cause.constraint` after trimming. + - Missing or blank constraint falls back to `"unknown"`. + - `23503` remains `ConstraintError`. +8. Run validation for this task: + - `pnpm lint-fix` + - `pnpm test packages/sql/pglite/test/SqlErrorClassification.test.ts` + - `pnpm check:tsgo` + +Rationale: + +- PGlite is similar to PostgreSQL but is a separate package. A dedicated task keeps validation focused and independently shippable. + +### Task 6: Update MySQL classification and tests + +Status: Completed. MySQL duplicate-entry errno `1062` is now classified as `UniqueViolation` before the generic constraint errno set. The classifier prefers a non-empty structured `cause.constraint`, then parses common duplicate-entry `for key ...` forms from `cause.sqlMessage` or `cause.message`, and falls back to exactly `"unknown"` for missing, blank, malformed, or non-string metadata. Other MySQL constraint errno values remain classified as `ConstraintError`. + +Validation completed for this task: + +- `pnpm lint-fix` +- `pnpm test packages/sql/mysql2/test/SqlErrorClassification.test.ts` +- `pnpm check:tsgo` + +Scope: + +- `packages/sql/mysql2/src/MysqlClient.ts`. +- `packages/sql/mysql2/test/SqlErrorClassification.test.ts`. + +Steps: + +1. Import `UniqueViolation` from `effect/unstable/sql/SqlError`. +2. Remove errno `1062` from the generic constraint classification path or check it before the constraint set. +3. Add helper logic to extract normalized `constraint`: + - Prefer `cause.constraint` if it is a non-empty string after trimming. + - Otherwise parse `cause.sqlMessage` or `cause.message` for `for key ...`. + - Otherwise return `"unknown"`. +4. Return `new UniqueViolation({ ...props, constraint })` for errno `1062`. +5. Keep other existing constraint errno values mapped to `ConstraintError`. +6. Update tests to assert: + - `1062` -> `UniqueViolation`. + - Constraint key extraction from representative duplicate-entry messages. + - Blank/missing/invalid message -> `"unknown"`. + - Another constraint errno, such as `1048` or `1452`, remains `ConstraintError`. +7. Run validation for this task: + - `pnpm lint-fix` + - `pnpm test packages/sql/mysql2/test/SqlErrorClassification.test.ts` + - `pnpm check:tsgo` + +Rationale: + +- MySQL has a distinct error shape and test mocking setup; this can ship independently after the core reason exists. + +### Task 7: Update MSSQL classification and tests + +Status: Completed. MSSQL error numbers `2601` and `2627` are now classified as `UniqueViolation` before generic constraint handling, with `515` and `547` retained as `ConstraintError`. The classifier normalizes structured `cause.constraint` values, parses common `constraint 'name'` and `unique index 'name'` messages for the respective MSSQL unique-violation numbers, and falls back to exactly `"unknown"` for missing, blank, malformed, or non-string metadata. Tests cover both unique numbers, structured-field precedence and trimming, fallback behavior, and the non-unique `547` constraint path. + +Validation completed for this task: + +- `pnpm lint-fix` +- `pnpm test packages/sql/mssql/test/SqlErrorClassification.test.ts` +- `pnpm check:tsgo` + +Scope: + +- `packages/sql/mssql/src/MssqlClient.ts`. +- `packages/sql/mssql/test/SqlErrorClassification.test.ts`. + +Steps: + +1. Import `UniqueViolation` from `effect/unstable/sql/SqlError`. +2. Remove numbers `2601` and `2627` from the generic constraint path or check them before the constraint set. +3. Add helper logic to extract normalized `constraint`: + - Prefer `cause.constraint` if it is a non-empty string after trimming. + - Parse `constraint 'name'` for `2627` messages. + - Parse `unique index 'name'` for `2601` messages. + - Otherwise return `"unknown"`. +4. Return `new UniqueViolation({ ...props, constraint })` for numbers `2601` and `2627`. +5. Keep other existing constraint numbers mapped to `ConstraintError`. +6. Update tests to assert: + - `2601` -> `UniqueViolation` and extracts unique index name. + - `2627` -> `UniqueViolation` and extracts constraint name. + - Structured `cause.constraint` is preferred and trimmed. + - Blank/missing/invalid message -> `"unknown"`. + - `547` remains `ConstraintError`. +7. Run validation for this task: + - `pnpm lint-fix` + - `pnpm test packages/sql/mssql/test/SqlErrorClassification.test.ts` + - `pnpm check:tsgo` + +Rationale: + +- MSSQL has a distinct error shape and test mocking setup; this can ship independently after the core reason exists. + +### Task 8: Add release documentation and final validation + +Status: Completed. The existing `.changeset/silver-snails-sqlite.md` was expanded to cover all directly affected release packages: `effect`, PostgreSQL, PGlite, MySQL, MSSQL, `@effect/sql-libsql`, and the SQLite-family packages using the shared SQLite classifier. The changeset now documents the new `UniqueViolation` SQL error reason, the classification change from `ConstraintError` to `UniqueViolation` for supported unique constraint violations, the exact `"unknown"` fallback, and the broadened PostgreSQL/PGlite/MySQL/MSSQL/shared-SQLite classification coverage. + +Validation completed for this release-metadata task: + +- `pnpm lint-fix` +- `pnpm check:tsgo` + +Scope: + +- Update the existing changeset under `.changeset/`. +- Run the final validation requested for this release-metadata-only task. + +Steps: + +1. Update the existing changeset for affected package(s). At minimum include `effect`, `@effect/sql-pg`, `@effect/sql-pglite`, `@effect/sql-mysql2`, and `@effect/sql-mssql`. This follow-up used exhaustive package entries for the SQLite-family packages whose classification changes through shared `classifySqliteError`. +2. The changeset must state: + - `UniqueViolation` was added as a new SQL error reason. + - Unique constraint violations now classify as `UniqueViolation` instead of `ConstraintError`. + - The reason includes `constraint`, falling back to `"unknown"`. +3. For this release-metadata-only follow-up, run the validation requested by the task: + - `pnpm lint-fix` + - `pnpm check:tsgo` + - If `pnpm check:tsgo` repeatedly fails due to stale caches, run `pnpm clean` and then rerun `pnpm check:tsgo`. + - The behavior tests and localized docgen were completed in the implementation tasks above; no source code changes were made in this follow-up. +4. Confirm no generated barrel files were manually edited. +5. Confirm no scratchpad files remain. + +Rationale: + +- Documentation and full validation should happen after all behavior changes are present; this task is independently reviewable as release metadata plus verification. + +## Risks and Mitigations + +1. Risk: Existing application code checks only `error.reason._tag === "ConstraintError"` for unique violations. + - Mitigation: Document the classification change in a changeset and release notes. +2. Risk: Internal consumers that currently catch `ConstraintError` regress. + - Mitigation: Update `Migrator.ts` and `SqlEventLogServerUnencrypted.ts` and run integration tests for the affected storage paths. +3. Risk: Driver message formats vary by database version or locale. + - Mitigation: Prefer structured properties, keep parsing conservative, normalize blank strings to `"unknown"`, and fall back to `"unknown"`. +4. Risk: SQLite numeric classification currently masks extended result codes to base codes. + - Mitigation: Check exact extended unique code `2067` before masking with `0xff`. +5. Risk: Tests that use common reason-case constructor types may not account for `constraint` being required. + - Mitigation: Adjust test helpers to support per-reason construction data or add focused `UniqueViolation` tests. +6. Risk: Adding `UniqueViolation` to schema union incorrectly can break schema encode/decode. + - Mitigation: Add schema roundtrip tests wrapping `UniqueViolation`. + +## Subagent Review Notes Incorporated + +Two subagents reviewed the specification and implementation plan. Their recommendations were incorporated as follows: + +- Added requirements and a dedicated task for internal consumers that currently catch `ConstraintError`. +- Split PostgreSQL and PGlite into separate independently shippable tasks. +- Added explicit blank/whitespace normalization rules for `constraint` extraction. +- Added explicit schema union placement guidance. +- Added SQLite primary-key-specific code scope guidance. +- Expanded validation to include event log storage tests. +- Made changeset package coverage and classification behavior change explicit. + +## Open Questions + +None. The user clarified that all supported drivers with specific unique violation identification should map to `UniqueViolation`, and that `constraint` may fall back to `"unknown"`. diff --git a/.repos/effect-smol/.vscode/extensions.json b/.repos/effect-smol/.vscode/extensions.json new file mode 100644 index 00000000000..1a118e9e73d --- /dev/null +++ b/.repos/effect-smol/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "oxc.oxc-vscode", + "dprint.dprint" + ] +} diff --git a/.repos/effect-smol/.vscode/settings.json b/.repos/effect-smol/.vscode/settings.json new file mode 100644 index 00000000000..7e6c0d2e887 --- /dev/null +++ b/.repos/effect-smol/.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-smol/AGENTS.md b/.repos/effect-smol/AGENTS.md new file mode 100644 index 00000000000..2df694e91a3 --- /dev/null +++ b/.repos/effect-smol/AGENTS.md @@ -0,0 +1,138 @@ +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:tsgo` | +| Tests-only changes | `pnpm lint-fix`, targeted `pnpm test `, `pnpm check:tsgo` | +| Type-level/API type changes | Targeted `pnpm test-types `, plus `pnpm check:tsgo` when source types changed | +| JSDoc/example changes | From the changed package directory, run `pnpm docgen`; also run `pnpm check:tsgo` when source types changed | +| Docs-only changes | `pnpm lint-fix`; no tests required unless examples or code changed | + +## 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-smol/CLAUDE.md b/.repos/effect-smol/CLAUDE.md new file mode 120000 index 00000000000..47dc3e3d863 --- /dev/null +++ b/.repos/effect-smol/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/.repos/effect-smol/LICENSE b/.repos/effect-smol/LICENSE new file mode 100644 index 00000000000..be1f5c14c7b --- /dev/null +++ b/.repos/effect-smol/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-smol/LLMS.md b/.repos/effect-smol/LLMS.md new file mode 100644 index 00000000000..4c9be04f47e --- /dev/null +++ b/.repos/effect-smol/LLMS.md @@ -0,0 +1,330 @@ +# 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. + +## 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/02_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/02_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/02_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/03_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/03_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/04_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/04_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/04_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/05_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/05_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/06_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/02_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/02_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/02_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/03_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`. + +## 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. + +## 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-smol/MIGRATION.md b/.repos/effect-smol/MIGRATION.md new file mode 100644 index 00000000000..902888a685e --- /dev/null +++ b/.repos/effect-smol/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-smol/README.md b/.repos/effect-smol/README.md new file mode 100644 index 00000000000..4ca81437140 --- /dev/null +++ b/.repos/effect-smol/README.md @@ -0,0 +1,3 @@ + + +[![pkg.pr.new](https://img.shields.io/badge/pkg.pr.new-Effect--TS%2Feffect--smol-black)](https://pkg.pr.new/~/Effect-TS/effect-smol) diff --git a/.repos/effect-smol/TODOS.md b/.repos/effect-smol/TODOS.md new file mode 100644 index 00000000000..4773f75396d --- /dev/null +++ b/.repos/effect-smol/TODOS.md @@ -0,0 +1,28 @@ +## Beta (Current) + +Pre-releases to npm from smol repo + +- [x] Add AI Embeddings module +- [ ] Comprehensive JSDoc on every exported function +- [ ] Codemod CLI for v3 migration + +## RC's + +Pre-releases to npm from smol repo + +## Release + +- [ ] Copy code over to main repo + +# Module Audit + +The exports under each section are organized as they are in Effect 3.0. The categorization of these modules may not be correct, and should be fixed for 4.0. + +### Legend + +| Status | Description | +| :----: | :----------------------------------------------- | +| - | Not done (default) | +| Done | Done - successfully ported to Effect 4 | +| X | Won't do - not being ported to Effect 4 | +| ? | Question - method has questions or uncertainties | diff --git a/.repos/effect-smol/ai-docs/README.md b/.repos/effect-smol/ai-docs/README.md new file mode 100644 index 00000000000..dd4488c6212 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/package.json b/.repos/effect-smol/ai-docs/package.json new file mode 100644 index 00000000000..0a0a320de79 --- /dev/null +++ b/.repos/effect-smol/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.18", + "nodemailer": "^8.0.7" + }, + "devDependencies": { + "@types/nodemailer": "^8.0.0" + } +} diff --git a/.repos/effect-smol/ai-docs/src/01_effect/01_basics/01_effect-gen.ts b/.repos/effect-smol/ai-docs/src/01_effect/01_basics/01_effect-gen.ts new file mode 100644 index 00000000000..4e45a893523 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/01_effect/01_basics/02_effect-fn.ts b/.repos/effect-smol/ai-docs/src/01_effect/01_basics/02_effect-fn.ts new file mode 100644 index 00000000000..dcbfe6d6240 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/01_effect/01_basics/10_creating-effects.ts b/.repos/effect-smol/ai-docs/src/01_effect/01_basics/10_creating-effects.ts new file mode 100644 index 00000000000..df07b62f7a5 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/01_effect/01_basics/index.md b/.repos/effect-smol/ai-docs/src/01_effect/01_basics/index.md new file mode 100644 index 00000000000..ae13d7a31ca --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/01_effect/02_services/01_service.ts b/.repos/effect-smol/ai-docs/src/01_effect/02_services/01_service.ts new file mode 100644 index 00000000000..1f4aec979fa --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/02_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-smol/ai-docs/src/01_effect/02_services/10_reference.ts b/.repos/effect-smol/ai-docs/src/01_effect/02_services/10_reference.ts new file mode 100644 index 00000000000..17d71d9006e --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/02_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-smol/ai-docs/src/01_effect/02_services/20_layer-composition.ts b/.repos/effect-smol/ai-docs/src/01_effect/02_services/20_layer-composition.ts new file mode 100644 index 00000000000..58ff1273c2b --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/02_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-smol/ai-docs/src/01_effect/02_services/20_layer-unwrap.ts b/.repos/effect-smol/ai-docs/src/01_effect/02_services/20_layer-unwrap.ts new file mode 100644 index 00000000000..bfa1677b749 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/02_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-smol/ai-docs/src/01_effect/02_services/index.md b/.repos/effect-smol/ai-docs/src/01_effect/02_services/index.md new file mode 100644 index 00000000000..ac938fc6d83 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/02_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-smol/ai-docs/src/01_effect/03_errors/01_error-handling.ts b/.repos/effect-smol/ai-docs/src/01_effect/03_errors/01_error-handling.ts new file mode 100644 index 00000000000..c5071188797 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/03_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-smol/ai-docs/src/01_effect/03_errors/10_catch-tags.ts b/.repos/effect-smol/ai-docs/src/01_effect/03_errors/10_catch-tags.ts new file mode 100644 index 00000000000..020473dd42c --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/03_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-smol/ai-docs/src/01_effect/03_errors/20_reason-errors.ts b/.repos/effect-smol/ai-docs/src/01_effect/03_errors/20_reason-errors.ts new file mode 100644 index 00000000000..bced427c132 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/03_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-smol/ai-docs/src/01_effect/03_errors/index.md b/.repos/effect-smol/ai-docs/src/01_effect/03_errors/index.md new file mode 100644 index 00000000000..14d16146d90 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/03_errors/index.md @@ -0,0 +1 @@ +## Error handling diff --git a/.repos/effect-smol/ai-docs/src/01_effect/04_resources/10_acquire-release.ts b/.repos/effect-smol/ai-docs/src/01_effect/04_resources/10_acquire-release.ts new file mode 100644 index 00000000000..1c446f760a5 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/04_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-smol/ai-docs/src/01_effect/04_resources/20_layer-side-effects.ts b/.repos/effect-smol/ai-docs/src/01_effect/04_resources/20_layer-side-effects.ts new file mode 100644 index 00000000000..3d3dfb8f5dc --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/04_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-smol/ai-docs/src/01_effect/04_resources/30_layer-map.ts b/.repos/effect-smol/ai-docs/src/01_effect/04_resources/30_layer-map.ts new file mode 100644 index 00000000000..173e6e4b938 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/04_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-smol/ai-docs/src/01_effect/04_resources/index.md b/.repos/effect-smol/ai-docs/src/01_effect/04_resources/index.md new file mode 100644 index 00000000000..cda2f507bd2 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/04_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-smol/ai-docs/src/01_effect/05_running/10_run-main.ts b/.repos/effect-smol/ai-docs/src/01_effect/05_running/10_run-main.ts new file mode 100644 index 00000000000..3e1786cb2d3 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/05_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-smol/ai-docs/src/01_effect/05_running/20_layer-launch.ts b/.repos/effect-smol/ai-docs/src/01_effect/05_running/20_layer-launch.ts new file mode 100644 index 00000000000..e92e1242dd2 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/05_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-smol/ai-docs/src/01_effect/05_running/index.md b/.repos/effect-smol/ai-docs/src/01_effect/05_running/index.md new file mode 100644 index 00000000000..e4c2c874cf0 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/05_running/index.md @@ -0,0 +1 @@ +## Running Effect programs diff --git a/.repos/effect-smol/ai-docs/src/01_effect/06_pubsub/10_pubsub.ts b/.repos/effect-smol/ai-docs/src/01_effect/06_pubsub/10_pubsub.ts new file mode 100644 index 00000000000..6bb876a54c7 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/06_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-smol/ai-docs/src/01_effect/06_pubsub/index.md b/.repos/effect-smol/ai-docs/src/01_effect/06_pubsub/index.md new file mode 100644 index 00000000000..22b0ce0370f --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/01_effect/06_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-smol/ai-docs/src/02_stream/10_creating-streams.ts b/.repos/effect-smol/ai-docs/src/02_stream/10_creating-streams.ts new file mode 100644 index 00000000000..1e085f99660 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/02_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-smol/ai-docs/src/02_stream/20_consuming-streams.ts b/.repos/effect-smol/ai-docs/src/02_stream/20_consuming-streams.ts new file mode 100644 index 00000000000..cfe8b92e464 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/02_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-smol/ai-docs/src/02_stream/30_encoding.ts b/.repos/effect-smol/ai-docs/src/02_stream/30_encoding.ts new file mode 100644 index 00000000000..16a6b6aa7af --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/02_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-smol/ai-docs/src/02_stream/index.md b/.repos/effect-smol/ai-docs/src/02_stream/index.md new file mode 100644 index 00000000000..710e143d502 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/02_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-smol/ai-docs/src/03_integration/10_managed-runtime.ts b/.repos/effect-smol/ai-docs/src/03_integration/10_managed-runtime.ts new file mode 100644 index 00000000000..ed29f83b09b --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/03_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-smol/ai-docs/src/03_integration/index.md b/.repos/effect-smol/ai-docs/src/03_integration/index.md new file mode 100644 index 00000000000..dd01cdbdb29 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/03_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-smol/ai-docs/src/05_batching/10_request-resolver.ts b/.repos/effect-smol/ai-docs/src/05_batching/10_request-resolver.ts new file mode 100644 index 00000000000..156e653d521 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/05_batching/index.md b/.repos/effect-smol/ai-docs/src/05_batching/index.md new file mode 100644 index 00000000000..4166039c959 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/06_schedule/10_schedules.ts b/.repos/effect-smol/ai-docs/src/06_schedule/10_schedules.ts new file mode 100644 index 00000000000..82c8655a225 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/06_schedule/10_schedules.ts @@ -0,0 +1,102 @@ +/** + * @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" + +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.both` continues only while both schedules continue. +// It is useful for combining a delay pattern with a hard attempt cap. +export const retryBackoffWithLimit = Schedule.both( + Schedule.exponential("250 millis"), + Schedule.recurs(6) +) + +// `Schedule.either` continues while either schedule continues. +// It is useful for fallback behavior (e.g. stop only when both are exhausted). +export const keepTryingUntilBothStop = Schedule.either( + Schedule.spaced("2 seconds"), + Schedule.recurs(3) +) + +// 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) +) + +// `tapInput` and `tapOutput` are useful for performing side effects like +// logging or metrics. +export const instrumentedRetrySchedule = retryableOnly.pipe( + Schedule.setInputType(), + Schedule.tapInput((error) => Effect.logDebug(`Retrying after ${error.status}: ${error.message}`)), + Schedule.tapOutput((delay) => Effect.logDebug(`Next retry in ${Duration.toMillis(delay)}ms`)) +) + +// 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.exponential("250 millis").pipe( + // Cap the delay at 10 seconds to avoid excessively long waits. + Schedule.either(Schedule.spaced("10 seconds")), + 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 +) diff --git a/.repos/effect-smol/ai-docs/src/06_schedule/index.md b/.repos/effect-smol/ai-docs/src/06_schedule/index.md new file mode 100644 index 00000000000..b56aa235c46 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/08_observability/10_logging.ts b/.repos/effect-smol/ai-docs/src/08_observability/10_logging.ts new file mode 100644 index 00000000000..7cff7a2c9bd --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/08_observability/20_otlp-tracing.ts b/.repos/effect-smol/ai-docs/src/08_observability/20_otlp-tracing.ts new file mode 100644 index 00000000000..121e795dbeb --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/08_observability/index.md b/.repos/effect-smol/ai-docs/src/08_observability/index.md new file mode 100644 index 00000000000..df0399ad453 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/09_testing/10_effect-tests.ts b/.repos/effect-smol/ai-docs/src/09_testing/10_effect-tests.ts new file mode 100644 index 00000000000..393d29daf59 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/09_testing/20_layer-tests.ts b/.repos/effect-smol/ai-docs/src/09_testing/20_layer-tests.ts new file mode 100644 index 00000000000..556dc8d51cf --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/09_testing/index.md b/.repos/effect-smol/ai-docs/src/09_testing/index.md new file mode 100644 index 00000000000..5df1fab5022 --- /dev/null +++ b/.repos/effect-smol/ai-docs/src/09_testing/index.md @@ -0,0 +1 @@ +## Testing Effect programs diff --git a/.repos/effect-smol/ai-docs/src/50_http-client/10_basics.ts b/.repos/effect-smol/ai-docs/src/50_http-client/10_basics.ts new file mode 100644 index 00000000000..3cb881ad854 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/50_http-client/index.md b/.repos/effect-smol/ai-docs/src/50_http-client/index.md new file mode 100644 index 00000000000..7baa9010f60 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/10_basics.ts b/.repos/effect-smol/ai-docs/src/51_http-server/10_basics.ts new file mode 100644 index 00000000000..79e92c9089d --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/fixtures/api/Api.ts b/.repos/effect-smol/ai-docs/src/51_http-server/fixtures/api/Api.ts new file mode 100644 index 00000000000..20140ae7cb7 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/fixtures/api/Authorization.ts b/.repos/effect-smol/ai-docs/src/51_http-server/fixtures/api/Authorization.ts new file mode 100644 index 00000000000..8b25c042901 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/fixtures/api/System.ts b/.repos/effect-smol/ai-docs/src/51_http-server/fixtures/api/System.ts new file mode 100644 index 00000000000..c66565b0655 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/fixtures/api/Users.ts b/.repos/effect-smol/ai-docs/src/51_http-server/fixtures/api/Users.ts new file mode 100644 index 00000000000..f3b56d4ef89 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/fixtures/domain/User.ts b/.repos/effect-smol/ai-docs/src/51_http-server/fixtures/domain/User.ts new file mode 100644 index 00000000000..852e2011e57 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts b/.repos/effect-smol/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts new file mode 100644 index 00000000000..fa2a757b2bc --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/fixtures/server/Authorization.ts b/.repos/effect-smol/ai-docs/src/51_http-server/fixtures/server/Authorization.ts new file mode 100644 index 00000000000..bbdbed39bb8 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/fixtures/server/Users.ts b/.repos/effect-smol/ai-docs/src/51_http-server/fixtures/server/Users.ts new file mode 100644 index 00000000000..a9833762c2a --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/fixtures/server/Users/http.ts b/.repos/effect-smol/ai-docs/src/51_http-server/fixtures/server/Users/http.ts new file mode 100644 index 00000000000..a1b7b3eacb6 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/51_http-server/index.md b/.repos/effect-smol/ai-docs/src/51_http-server/index.md new file mode 100644 index 00000000000..e926d996061 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/60_child-process/10_working-with-child-processes.ts b/.repos/effect-smol/ai-docs/src/60_child-process/10_working-with-child-processes.ts new file mode 100644 index 00000000000..f305b345aee --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/60_child-process/index.md b/.repos/effect-smol/ai-docs/src/60_child-process/index.md new file mode 100644 index 00000000000..8aafdd603f9 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/70_cli/10_basics.ts b/.repos/effect-smol/ai-docs/src/70_cli/10_basics.ts new file mode 100644 index 00000000000..a4fe368acc9 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/70_cli/index.md b/.repos/effect-smol/ai-docs/src/70_cli/index.md new file mode 100644 index 00000000000..96a3b756f62 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/71_ai/10_language-model.ts b/.repos/effect-smol/ai-docs/src/71_ai/10_language-model.ts new file mode 100644 index 00000000000..2f89956bb74 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/71_ai/20_tools.ts b/.repos/effect-smol/ai-docs/src/71_ai/20_tools.ts new file mode 100644 index 00000000000..0acd8e97c05 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/71_ai/30_chat.ts b/.repos/effect-smol/ai-docs/src/71_ai/30_chat.ts new file mode 100644 index 00000000000..600017024b6 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts b/.repos/effect-smol/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts new file mode 100644 index 00000000000..6dd8d33c6ae --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/71_ai/index.md b/.repos/effect-smol/ai-docs/src/71_ai/index.md new file mode 100644 index 00000000000..425f4b46bfc --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/80_cluster/10_entities.ts b/.repos/effect-smol/ai-docs/src/80_cluster/10_entities.ts new file mode 100644 index 00000000000..a1be374a5e8 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/80_cluster/index.md b/.repos/effect-smol/ai-docs/src/80_cluster/index.md new file mode 100644 index 00000000000..1cc42cb54c2 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/src/index.md b/.repos/effect-smol/ai-docs/src/index.md new file mode 100644 index 00000000000..092209d7573 --- /dev/null +++ b/.repos/effect-smol/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-smol/ai-docs/tsconfig.json b/.repos/effect-smol/ai-docs/tsconfig.json new file mode 100644 index 00000000000..94d60356d8d --- /dev/null +++ b/.repos/effect-smol/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-smol/cookbooks/schedule.md b/.repos/effect-smol/cookbooks/schedule.md new file mode 100644 index 00000000000..4923efa7053 --- /dev/null +++ b/.repos/effect-smol/cookbooks/schedule.md @@ -0,0 +1,24462 @@ +# Schedule Cookbook + +Use GitHub's document outline or browser search to jump to the recipe matching +your problem. If you are new to `Schedule`, start with Part I. Otherwise, +search for the shape of your problem, such as retry, repeat, polling, backoff, +jitter, timeout, or recurrence limit. + +Watch for four beginner traps throughout the recipes: + +- **Channel choice** — `repeat` observes successes; `retry` observes typed + failures. +- **Recurrence counts** — schedules count recurrences after the first execution. +- **Schedule output** — schedule output is not always the business result. +- **Bounds** — unbounded schedules need a limit, owner, or interruption path. + +## Part I — Foundations + +### 1. What a `Schedule` Really Represents + +#### 1.1 Recurrence policies as data + +A `Schedule` is a value that describes when to run something again. It says +whether another run is allowed, how long to wait before it, and what value the +schedule reports. It does not perform the work being retried, repeated, or +polled. + +##### Problem + +Recurrence rules are easy to hide in loops, callbacks, and scattered sleeps. +That makes them hard to reuse and hard to review. A schedule keeps those rules +separate from the effect that performs the work. + +The work answers "what should happen now?" The schedule answers "should there be +another opportunity, when should it happen, and what did the policy report?" + +##### Model + +At the type level, a schedule has the shape +`Schedule.Schedule`. + +Read them from the policy's point of view: + +- `Output` is the value emitted by the schedule, such as a count, duration, or + label. +- `Input` is the value fed to the schedule by the driver. +- `Error` is an error raised by schedule logic itself. +- `Env` is any Effect context required by the schedule. + +Most common schedules are simpler than the full type suggests. +`Schedule.recurs(3)`, `Schedule.spaced("1 second")`, and `Schedule.forever` +ignore their input and output counts. Backoff schedules such as +`Schedule.exponential("100 millis")` output durations. + +Because a schedule is a value, you can name it, pass it around, transform it, +and compose it before any recurrence happens. + +##### Example + +This example defines two policies first, then attaches them to effects: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +const retryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.recurs(4)) +) + +const refreshPolicy = Schedule.spaced("10 millis").pipe( + Schedule.take(2) +) + +let attempts = 0 + +const flakyRequest = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`request attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail("temporary outage") + } + + return "response" +}) + +const refresh = Console.log("refresh cache") + +const program = Effect.gen(function*() { + const response = yield* flakyRequest.pipe( + Effect.retry(retryPolicy) + ) + yield* Console.log(`retry result: ${response}`) + + const refreshOutput = yield* refresh.pipe( + Effect.repeat(refreshPolicy) + ) + yield* Console.log(`refresh schedule output: ${refreshOutput}`) +}) + +Effect.runPromise(program) +// Output: +// request attempt 1 +// request attempt 2 +// request attempt 3 +// retry result: response +// refresh cache +// refresh cache +// refresh cache +// refresh schedule output: 2 +``` + +##### Common mistakes + +A schedule value is not a plain JSON object. Some schedules carry internal step +state while they are being driven. The useful point is that the policy is +first-class: it can be named, reviewed, reused, and combined before a driver such +as `Effect.retry` or `Effect.repeat` runs it. + +Other common mistakes are: + +- putting timing and stopping rules in the effect body when they belong in the + schedule; +- treating schedule output as the business result of the repeated or retried + effect; +- assuming a schedule is only a sleep, when schedules can count, inspect inputs, + emit durations, transform outputs, and compose with other policies. + +##### Practical guidance + +Name the rule for running again before attaching it to the work. Start with the +smallest pieces, such as a cadence and a limit, then compose them. + +When reading schedule code, ask: + +- What rule for running again am I declaring? +- What does the policy output? +- What input does the policy need to observe? +- Which constraints should be composed instead of embedded in the effect body? + +If the answer is mostly about timing, counting, stopping, or observing recurrence +inputs, it belongs in a `Schedule`. If the answer is about the business action, +keep it in the effect that the schedule drives. + +#### 1.2 The input/output view of a schedule + +A schedule is easier to read when you separate the value it observes from the +value it emits. In `Schedule.Schedule`, `Input` is +what the driver feeds to the policy, and `Output` is what the policy reports. + +Beginner note: Schedule output — the schedule reports policy information; it is +not automatically the successful value produced by the effect being repeated or +retried. + +##### Problem + +Developers often read a schedule only as a delay. That misses an important +part of the model: schedules can receive values and report values. The input is +not the constructor argument in `Schedule.spaced("1 second")`; it is the value +passed to the schedule each time it is stepped. + +##### Model + +For cookbook usage, read the first two type parameters first: + +| Type | Meaning | +| -------- | ----------------------------------------------------------------- | +| `Input` | The value supplied to the schedule at each decision point. | +| `Output` | The value emitted by the schedule when it continues or completes. | + +`Effect.retry` feeds typed failures into the schedule. `Effect.repeat` feeds +successful values into the schedule. A schedule that ignores input can usually +be used with either entry point. A schedule that inspects input must match the +channel selected by the driver. + +Common constructor outputs are also worth knowing: + +| Schedule | Common output | +| --------------------------------------------------------------- | ----------------- | +| `Schedule.recurs`, `Schedule.spaced`, `Schedule.fixed` | recurrence counts | +| `Schedule.forever` | recurrence counts | +| `Schedule.exponential`, `Schedule.duration`, `Schedule.elapsed` | durations | +| `Schedule.passthrough(schedule)` | the latest input | + +##### Example + +This repeat policy receives successful values. `Schedule.passthrough` turns the +latest input into the schedule output, then `Schedule.map` changes the output +into a log-friendly label: + + + +```ts no-check +import { Console, Effect, Schedule } from "effect" + +type Status = "warming" | "ready" + +let polls = 0 + +const readStatus = Effect.sync((): Status => { + polls += 1 + return polls < 3 ? "warming" : "ready" +}).pipe( + Effect.tap((status) => Console.log(`effect success: ${status}`)) +) + +const program = Effect.gen(function*() { + const scheduleOutput = yield* readStatus.pipe( + Effect.repeat(($) => + Schedule.passthrough($(Schedule.forever)).pipe( + Schedule.tapInput((status) => Console.log(`schedule input: ${status}`)), + Schedule.map((status) => `schedule output: saw ${status}`), + Schedule.tapOutput((message) => Console.log(message)), + Schedule.while(({ input }) => input !== "ready") + ) + ) + ) + + yield* Console.log(`repeat returned: ${scheduleOutput}`) +}) + +Effect.runPromise(program) +``` + +The effect succeeds with `"warming"`, `"warming"`, then `"ready"`. Each success +is schedule input. The final success also becomes the final schedule output +after mapping, because the raw schedule overload of `Effect.repeat` returns the +schedule output. + +##### Common mistakes + +Schedule output is not automatically the business value produced by the effect. +With `Effect.retry`, the retried effect still succeeds with the original +successful value. With the raw schedule overload of `Effect.repeat`, the result +is the schedule output. With the options form of `Effect.repeat`, the result is +the last successful value of the repeated effect. + +Another common mistake is treating the delay as the output. A schedule decision +contains both an output and the delay before the next recurrence, but only some +schedules choose to output durations. + +##### Practical guidance + +Before choosing combinators, ask two questions: + +- What value will the schedule receive: a success, an error, or some other + input from a lower-level driver? +- What should the schedule report: a count, a duration, the latest input, a + label, or a combined value? + +Use `tapInput` to observe inputs without changing the result, `tapOutput` to +observe outputs, `map` to transform outputs, and `passthrough` when the input +itself is the useful output. + +#### 1.3 Time, repetition, and decision points + +A `Schedule` is stepped between executions of some other effect. It does not run +the effect. It receives the latest input, updates its own recurrence state, and +decides whether another execution is allowed. + +##### Problem + +Time-based recurrence is often described as "sleep, then try again." That is too +small a model for `Schedule`. A schedule decision says whether to keep going, +what value to report, and how long to wait. Those are related, but they are not +the same thing. + +##### Model + +Each successful schedule decision answers three questions: + +- What input did the policy observe? +- What output did the policy emit? +- How long should the driver wait before the next recurrence? + +If the policy is done, there is no next recurrence. A zero delay means +"continue immediately"; it does not mean "stop." + +For retry, the decision point happens after a typed failure. The failure is the +schedule input. For repeat, the decision point happens after a success. The +successful value is the schedule input. In both cases, the first execution +happens before the first schedule decision. + +That rule is the source of the common count distinction: +`Schedule.recurs(3)` allows up to three recurrences after the initial execution. +In retry code, that means up to three retries. In repeat code, it means up to +three repetitions. + +Beginner note: Recurrence counts — when a requirement says "run three times +total", the schedule usually needs `recurs(2)` because the first execution has +already happened. + +##### Time + +Schedule time is measured at the step boundary. The schedule receives the +current timestamp and the latest input, then computes its output and next delay. +This lets time-based and count-based policies compose cleanly. + +Common timing policies have different meanings: + +- `Schedule.spaced(duration)` waits the same amount after each recurrence. +- `Schedule.fixed(duration)` aligns recurrences to fixed time windows. +- `Schedule.exponential(base)` increases the delay from one decision to the + next. +- `Schedule.duration(duration)` recurs once after the configured duration, then + completes. +- `Schedule.during(duration)` continues while elapsed schedule time remains + within the configured duration. +- `Schedule.elapsed` emits elapsed time as its output. + +These policies still produce schedule decisions. The delay answers when the next +run may happen. The continue-or-stop decision answers whether it may happen at +all. The output answers what the policy reports to the driver or later +combinators. + +##### Common mistakes + +The first mistake is counting the initial effect execution as a schedule step. +The effect runs once first. Only then does the schedule decide whether another +run is allowed. + +The second mistake is treating delay and "keep going" as the same thing. A +schedule can continue immediately, continue after a delay, or complete. Only the +last case stops recurrence. + +The third mistake is reading schedule output as elapsed time in every case. +Some schedules output durations, but many output counts or transformed values. + +##### Practical guidance + +When reading a schedule, translate it into a decision: + +- What input does this decision observe? +- What condition lets it continue? +- What delay does it choose for the next recurrence? +- What output does it publish? + +This framing keeps retry, repeat, polling, backoff, jitter, and elapsed-time +limits as variations of the same model instead of separate control-flow tricks. + +#### 1.4 Why `Schedule` is more than “retry with delay” + +Retry with a delay is one use of `Schedule`, not the definition of it. A +schedule is a reusable rule for running again. It can drive retrying, repeating, +polling, stream pacing, staged behavior, and observability. + +##### Problem + +A delay answers one question: "How long should I wait?" A schedule can also +answer "Should I continue?", "What input did I observe?", "What output should I +publish?", and "How does this policy combine with another policy?" + +Reducing `Schedule` to a sleep duration hides those decisions. + +##### Model + +A schedule step receives an input and timing metadata, then either continues +with an output plus a delay or completes with a final output. That model supports +several policy concerns: + +- continuation with `Schedule.recurs`, `Schedule.take`, `Schedule.during`, and + `Schedule.while`; +- timing with `Schedule.spaced`, `Schedule.fixed`, `Schedule.windowed`, + `Schedule.exponential`, `Schedule.fibonacci`, `Schedule.cron`, and + `Schedule.duration`; +- output transformation and observation with `Schedule.map`, + `Schedule.tapInput`, and `Schedule.tapOutput`; +- output collection or accumulation with `Schedule.collectOutputs` and + `Schedule.reduce`; +- policy composition with `Schedule.both`, `Schedule.either`, and + `Schedule.andThen`. + +The same value can therefore express a bounded backoff retry policy, a polling +cadence, or a two-phase loop. The driver decides whether successful values or +typed failures are fed to the policy. + +##### Composition + +Composition is the part that a raw delay cannot express. + +`Schedule.both(left, right)` continues only while both policies continue. When +both produce delays, the combined delay is the maximum. This is useful for +"back off, but stop after five recurrences." + +`Schedule.either(left, right)` continues while either policy continues. Its +combined delay uses the minimum delay. This is useful when one policy may keep a +loop alive after another policy has completed. + +`Schedule.andThen(left, right)` is sequential. It runs the first policy to +completion, then runs the second. This is the right model for warm-up behavior +followed by a steadier cadence. + +##### Common mistakes + +The first mistake is treating schedule output as disposable. Counts, durations, +labels, accumulated state, and collected values can be useful for logging, +metrics, fallback decisions, and tests. + +The second mistake is assuming schedules only see failures. `Effect.retry` feeds +failures into a schedule, but `Effect.repeat` feeds successes. A successful job +state such as `"pending"` belongs in a repeat loop, not in a fake error used only +to make retry inspect it. + +The third mistake is encoding every policy in effect control flow. Once timing, +limits, predicates, or phases matter, a schedule value is usually easier to +inspect than a hand-written loop. + +##### Practical guidance + +Reach for `Schedule` when the rule for running again is more important than a +single sleep. Name the policy in recurrence terms: bounded backoff, fixed +polling, warm-up then steady state, retry while transient, repeat until terminal. + +If the only requirement is one hard-coded pause, a duration or `Effect.sleep` +may be enough. If the requirement includes whether to keep going, what to +report, how to compose policies, or how to inspect input values, model it as a +schedule. + +#### 1.5 Composability as the core design idea + +`Schedule` is designed around small policies that can be combined. A retry or +repeat policy often has several concerns: a cadence, a limit, a predicate, +observability, and sometimes phases. Each concern can be represented separately. + +##### Problem + +When recurrence logic is written as one loop, the policy becomes hard to read. +You have to inspect control flow to answer basic questions: what is the delay, +what stops the loop, and what happens after the first phase? + +Schedules make those relationships explicit. + +##### Core combinators + +Choose the combinator by the relationship between policies: + +- `Schedule.both` means both policies must continue. The combined delay is the + maximum delay. +- `Schedule.either` means either policy may continue. The combined delay is the + minimum delay. +- `Schedule.andThen` means the policies run sequentially: first one, then the + other. + +Use the output-selecting variants when a tuple is not useful: +`bothLeft`, `bothRight`, `bothWith`, `eitherLeft`, `eitherRight`, and +`eitherWith`. + +##### Example + +This retry policy has three separate concerns: a fast phase, a slower phase, and +a hard retry limit. + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class TemporaryError extends Data.TaggedError("TemporaryError")<{ + readonly attempt: number +}> {} + +const burstThenSlow = Schedule.spaced("10 millis").pipe( + Schedule.take(2), + Schedule.andThen( + Schedule.spaced("25 millis").pipe(Schedule.take(2)) + ) +) + +const retryPolicy = burstThenSlow.pipe( + Schedule.bothLeft(Schedule.recurs(4)), + Schedule.tapOutput((step) => Console.log(`policy step ${step}`)) +) + +let attempts = 0 + +const request = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`request attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail(new TemporaryError({ attempt: attempts })) + } + + return "ok" +}) + +const program = Effect.gen(function*() { + const result = yield* request.pipe( + Effect.retry(retryPolicy) + ) + yield* Console.log(`result: ${result}`) +}) + +Effect.runPromise(program) +// Output: +// request attempt 1 +// policy step 0 +// request attempt 2 +// policy step 1 +// request attempt 3 +// policy step 0 +// request attempt 4 +// result: ok +``` + +The timing policy is phased with `andThen`. The retry budget is added with +`bothLeft`, so both the timing policy and the count limit must allow another +retry. + +##### Common mistakes + +Do not use `both` when the intended behavior is phased. `both` runs policies at +the same time and stops when either one stops. Use `andThen` for "do this first, +then switch to that." + +Do not ignore output shape. `both` and `either` return tuples. That is useful +when both outputs matter, but noisy when the caller only needs one side or a +custom value. + +Do not combine many policies before naming the smaller pieces. Names make the +relationship between delay, limits, predicates, and phases visible. + +##### Practical guidance + +Build schedules in this order: + +1. Start with the cadence or backoff. +2. Add the stopping policy. +3. Add input predicates if the policy should inspect successes or failures. +4. Add output mapping or tapping for observability. +5. Sequence phases with `andThen` only when the policy really changes over time. + +The result should read like a rule for running again, not like hidden control +flow. + +### 2. `repeat` vs `retry` + +#### 2.1 Repeating successful effects + +Use `Effect.repeat` when a successful result should be followed by another run. +The schedule is consulted after success, not after failure. + +Beginner note: Channel choice — choose `repeat` only when the value you need to +inspect is a success value, such as a normal polling status. + +##### Problem + +Manual repetition tends to mix the unit of work with cadence, stopping rules, +and sleeps. `Effect.repeat` keeps the effect focused on one successful run while +a `Schedule` decides whether another successful run should follow. + +##### When to use it + +Use `repeat` for workflows where success means "consider doing this again": +heartbeats, periodic refreshes, metric sampling, polling successful domain +states, and bounded setup checks. + +A job status such as `"pending"` is usually a normal successful response, not +an error. + +##### When not to use it + +Do not use `repeat` to recover from failure. If the effect fails, repetition +stops immediately and the failure is returned. Use `Effect.retry` when the next +run should be triggered by a typed failure. + +Do not use an unbounded repeat for a one-shot workflow unless some surrounding +fiber, timeout, or interruption boundary is responsible for stopping it. + +##### Schedule shape + +`Effect.repeat` runs the effect once before the schedule makes a decision. After +each success, the successful value becomes schedule input. + +`Schedule.recurs(n)` allows up to `n` repetitions after the first run. +`Schedule.spaced(duration)` repeats indefinitely with that delay between +successful runs. Pair unbounded timing schedules with `times`, `take`, +`recurs`, or a predicate when the loop must finish on its own. + +The return value depends on the overload. The raw schedule overload returns the +schedule output. The options form, such as `Effect.repeat({ times: n })` or +`Effect.repeat({ schedule })`, returns the final successful value from the +effect. + +##### Example + +This heartbeat runs once immediately, then repeats twice more: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let beats = 0 + +const heartbeat = Effect.sync(() => { + beats += 1 + return `heartbeat ${beats}` +}).pipe( + Effect.tap((message) => Console.log(message)) +) + +const program = Effect.gen(function*() { + const lastValue = yield* heartbeat.pipe( + Effect.repeat({ + schedule: Schedule.spaced("10 millis"), + times: 2 + }) + ) + + yield* Console.log(`repeat returned last value: ${lastValue}`) +}) + +Effect.runPromise(program) +// Output: +// heartbeat 1 +// heartbeat 2 +// heartbeat 3 +// repeat returned last value: heartbeat 3 +``` + +The initial execution is not counted as a scheduled recurrence. The example +runs three times total: one initial heartbeat plus two repetitions. + +##### Variants + +Use `times` for the smallest bounded repeat when you care about the final +successful value. Use a raw schedule when you care about the schedule output. + +Use `until` when the successful value describes the stopping condition. Use +`while` when the successful value describes the condition for continuing. Both +predicates inspect successes when used with `repeat`. + +For polling, keep normal domain states in the success channel. If the status is +`"pending"`, repeat the successful polling effect until it returns a terminal +state. If the polling request itself fails, `repeat` returns that failure unless +the polling effect handles or retries it internally. + +##### Notes and caveats + +Delays are between recurrences. They do not delay the initial execution. + +When `until` or `while` is combined with a bounded schedule, repetition can end +because the predicate stopped it or because the schedule was exhausted. If the +caller must distinguish those outcomes, make that distinction explicit in the +success value or use a schedule output that records it. + +If the schedule itself can fail, that failure is part of the returned effect's +error channel. Basic schedules such as `Schedule.recurs` and `Schedule.spaced` +do not add their own error. + +#### 2.2 Retrying failed effects + +Use `Effect.retry` when a typed failure may be temporary and the same effect may +be attempted again safely. + +##### Problem + +Retrying is not the same as repeating. `retry` is driven by failures. The +original effect runs once. If it succeeds, retrying is never started. If it +fails with a typed error, the retry policy decides whether another attempt is +allowed. + +##### When to use it + +Use `retry` for transient inability to complete an operation: temporary network +errors, rate limits modeled as typed failures, reconnect attempts, resource +contention, or startup dependencies that may become available soon. + +Put the retry around the smallest operation that is safe to run more than once. +Retrying an entire workflow can duplicate side effects that already succeeded. + +##### When not to use it + +Do not use `retry` for successful domain states. A successful `"pending"` status +should usually be repeated or polled, not turned into an error only so retry can +see it. + +Do not rely on retry for defects or interruptions. `Effect.retry` retries typed +failures from the error channel; defects and interruptions are not retried as +typed failures. + +Beginner note: Channel choice — `retry` is for temporary inability to complete +an operation, not for ordinary states like `"pending"`, `"starting"`, or +`"warming"`. + +##### Schedule shape + +The schedule input is the typed failure from the failed attempt. If a later +attempt succeeds, the whole effect succeeds with that value. If the schedule is +exhausted while attempts are still failing, the last typed failure is returned. + +`times: 3` and `Schedule.recurs(3)` both mean up to three retries after the first +attempt. The effect may run four times total. + +Use a raw schedule when timing, composition, or reuse matters. Use options such +as `while`, `until`, and `times` when the policy is local to one call site. + +##### Example + +This request fails twice with a retryable error, then succeeds: + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class HttpError extends Data.TaggedError("HttpError")<{ + readonly status: number + readonly retryable: boolean +}> {} + +let attempts = 0 + +const request = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`request attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail( + new HttpError({ status: 503, retryable: true }) + ) + } + + return "response body" +}) + +const retryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const program = Effect.gen(function*() { + const body = yield* request.pipe( + Effect.retry({ + schedule: retryPolicy, + while: (error) => error.retryable + }) + ) + + yield* Console.log(`retry result: ${body}`) +}) + +Effect.runPromise(program) +// Output: +// request attempt 1 +// request attempt 2 +// request attempt 3 +// retry result: response body +``` + +The `while` predicate is checked after each typed failure. If it returns +`false`, retrying stops and that error is returned. If it returns `true`, the +schedule still has to allow another attempt. + +##### Common mistakes + +- Counting `times` or `Schedule.recurs` as total executions. They count retries + after the first attempt. +- Expecting retry to continue after success. The first success completes the + whole effect. +- Retrying a larger workflow when only one operation is idempotent. +- Using an unbounded schedule when an operational limit is required. +- Treating defects or interruptions as retryable typed failures. + +##### Practical guidance + +Use `retry` when the failure is expected to be temporary and the operation is +safe to attempt again. Add a count, elapsed-time budget, delay, backoff, or +jitter when retrying crosses a process or network boundary. + +If all attempts fail and you need a fallback value or recovery effect, use +`Effect.retryOrElse`. Plain `Effect.retry` preserves the final failure. + +#### 2.3 When the distinction matters + +`Effect.repeat` and `Effect.retry` both accept schedules, but they feed different +values to those schedules. The entry point is a semantic choice, not just a +timing choice. + +##### Problem + +A policy can only inspect the kind of value you give it. `repeat` gives it +successes. `retry` gives it failures. Polling states belong on the success path. +Transient service errors belong on the failure path. If the operator is wrong, +the schedule may never see the value you meant to inspect. + +##### Comparison + +| Question | `Effect.repeat` | `Effect.retry` | +| ---------------------------------------- | ------------------------------------------- | --------------------------------- | +| What triggers the schedule? | A successful value | A typed failure | +| What does the schedule receive as input? | The success value | The error value | +| What stops immediately? | The first failure | The first success | +| What happens when the schedule stops? | Repetition completes after the last success | Retry fails with the last error | +| What does `times: n` mean? | Up to `n` repetitions after the first run | Up to `n` retries after first run | + +The same real-world workflow can use either operator depending on how the result +is modeled. + +##### Example + +This program uses `repeat` for successful job states and `retry` for transient +service failures: + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +type JobState = "pending" | "ready" + +let polls = 0 + +const checkJob = Effect.sync((): JobState => { + polls += 1 + return polls < 3 ? "pending" : "ready" +}).pipe( + Effect.tap((state) => Console.log(`job state: ${state}`)) +) + +class ReportError extends Data.TaggedError("ReportError")<{ + readonly kind: "Unavailable" | "Unauthorized" +}> {} + +let attempts = 0 + +const fetchReport = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`report attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail(new ReportError({ kind: "Unavailable" })) + } + + return "report" +}) + +const retryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.recurs(4)) +) + +const program = Effect.gen(function*() { + const finalState = yield* checkJob.pipe( + Effect.repeat({ + schedule: Schedule.spaced("10 millis"), + until: (state) => state === "ready" + }) + ) + yield* Console.log(`repeat finished with: ${finalState}`) + + const report = yield* fetchReport.pipe( + Effect.retry({ + schedule: retryPolicy, + while: (error) => error.kind === "Unavailable" + }) + ) + yield* Console.log(`retry finished with: ${report}`) +}) + +Effect.runPromise(program) +// Output: +// job state: pending +// job state: pending +// job state: ready +// repeat finished with: ready +// report attempt 1 +// report attempt 2 +// report attempt 3 +// retry finished with: report +``` + +`"pending"` is a successful value, so the polling loop repeats. `"Unavailable"` +is a typed failure, so the request retries. + +##### Tradeoffs + +`repeat` keeps normal domain states in the success channel. That is a good fit +for polling, heartbeats, refresh loops, and workflows where a successful +observation decides whether to continue. The tradeoff is that the first failure +stops the repeat unless the repeated effect handles it. + +`retry` keeps transient inability to complete the operation in the error +channel. That is a good fit for requests, reconnect attempts, and resource +contention. The tradeoff is that success ends the retry immediately. + +##### Recommended default + +Put expected domain states in the success channel and repeat over them. Put +temporary inability to complete the operation in the error channel and retry over +it. + +If you find yourself failing with normal states only so `retry` can see them, or +turning real failures into successful values only so `repeat` can see them, the +model is probably carrying the wrong information in the wrong channel. + +Both operators run the effect once before the schedule makes a recurrence +decision. `times: 3` therefore means the initial execution plus up to three more +executions. + +#### 2.4 Common beginner mistakes + +Most early mistakes come from mixing three separate things: the success channel, +the error channel, and the schedule output. + +##### Problem + +`Effect.repeat` schedules successes. `Effect.retry` schedules typed failures. +Count-based policies count recurrences after the first execution. The raw +schedule overload of `Effect.repeat` returns the schedule output, not the effect +value. + +Those rules are small, but confusing them changes behavior and types. + +Beginner note: Channel choice — if a recipe surprises you, first ask which +channel the schedule is observing, then ask what value the operator returns. + +##### Mistakes to avoid + +| Mistake | Consequence | +| ---------------------------------------------------- | ----------------------------------------------- | +| Using `repeat` to recover from failure | The first failure is returned immediately. | +| Using `retry` for a successful polling state | The first success ends the retry. | +| Counting `times` as total executions | The effect may run one more time than expected. | +| Expecting raw `repeat(schedule)` to return the value | It returns the schedule output. | +| Putting predicates on the wrong operator | The predicate inspects the wrong channel. | +| Forgetting a schedule is unbounded | The loop runs until failure or interruption. | + +##### Example + +This small program shows three of the common surprises: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +const program = Effect.gen(function*() { + let repeatRuns = 0 + + const lastValue = yield* Effect.sync(() => { + repeatRuns += 1 + return `repeat run ${repeatRuns}` + }).pipe( + Effect.repeat({ times: 2 }) + ) + + yield* Console.log( + `repeat ran ${repeatRuns} times and returned "${lastValue}"` + ) + + let retryAttempts = 0 + + const retryExit = yield* Effect.failSync(() => { + retryAttempts += 1 + return "temporary" + }).pipe( + Effect.retry({ times: 2 }), + Effect.exit + ) + + yield* Console.log( + `retry attempted ${retryAttempts} times and ended with ${retryExit._tag}` + ) + + const rawScheduleOutput = yield* Effect.succeed("done").pipe( + Effect.repeat(Schedule.recurs(2)) + ) + + yield* Console.log( + `raw schedule repeat returned ${rawScheduleOutput}` + ) + + const repeatExit = yield* Effect.fail("temporary").pipe( + Effect.repeat(Schedule.recurs(2)), + Effect.exit + ) + + yield* Console.log( + `repeat over failure ended with ${repeatExit._tag}` + ) +}) + +Effect.runPromise(program) +// Output: +// repeat ran 3 times and returned "repeat run 3" +// retry attempted 3 times and ended with Failure +// raw schedule repeat returned 2 +// repeat over failure ended with Failure +``` + +`times: 2` allows two recurrences after the initial run. The retry example also +attempts three executions total. The raw schedule repeat returns the final +`Schedule.recurs` output. + +##### Predicate placement + +Predicates in `repeat` options inspect successful values: +`Effect.repeat({ until: (value) => ... })`. + +Predicates in `retry` options inspect typed failures: +`Effect.retry({ while: (error) => ... })`. + +Use `until` when the predicate describes the stopping condition. Use `while` +when it describes the condition for continuing. + +##### Practical guidance + +Use this checklist before choosing the operator: + +| If you mean... | Prefer... | +| ---------------------------------------------- | ----------------------------------------------------------- | +| Try again after a typed failure | `Effect.retry` | +| Run again after a success | `Effect.repeat` | +| Keep the last successful value from repetition | `Effect.repeat({ times })` or `Effect.repeat({ schedule })` | +| Use the schedule's output as the result | `Effect.repeat(schedule)` | +| Limit a retry or repeat to `n` more runs | `Schedule.recurs(n)` or `{ times: n }` | + +For an external requirement like "try three times total", subtract the initial +run from the recurrence count. That means `times: 2` or `Schedule.recurs(2)`. + +#### 2.5 Choosing the right entry point + +Choose the entry point by the channel the policy must observe. Timing comes +after that choice. + +##### Problem + +The same schedule value can often be passed to `Effect.repeat` or +`Effect.retry`, but the two operators feed it different inputs. A policy that +should inspect successful statuses belongs on `repeat`. A policy that should +inspect transient typed failures belongs on `retry`. + +##### Decision table + +| Question | Entry point | +| -------------------------------------------- | --------------- | +| Should the policy inspect successful values? | `Effect.repeat` | +| Should another run follow a success? | `Effect.repeat` | +| Should the first failure stop the loop? | `Effect.repeat` | +| Should the policy inspect typed failures? | `Effect.retry` | +| Should another run follow a typed failure? | `Effect.retry` | +| Should the first success stop the loop? | `Effect.retry` | + +##### Schedule shape + +Both entry points accept an options object, a `Schedule`, or a schedule builder. + +Use the options form when the policy is local: + +- `times` limits recurrences after the first execution. +- `while` continues while the observed value satisfies a predicate. +- `until` continues until the observed value satisfies a predicate. +- `schedule` adds an explicit schedule policy. + +The observed value depends on the entry point. In `repeat`, `while` and `until` +inspect successful values. In `retry`, they inspect typed failures. + +Use a named `Schedule` when the policy is reusable or composed from several +concerns. Use the builder form when the schedule needs to inspect its input and +you want that input type inferred from the effect. + +Return values are different. `Effect.retry` succeeds with the original effect's +successful value. The raw schedule overload of `Effect.repeat` succeeds with the +schedule output. The options form of `Effect.repeat` keeps the repeated effect's +final successful value. + +##### Example + +This program uses both entry points for their intended channels: + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +type Status = "starting" | "ready" + +let statusChecks = 0 + +const readStatus = Effect.sync((): Status => { + statusChecks += 1 + return statusChecks < 3 ? "starting" : "ready" +}).pipe( + Effect.tap((status) => Console.log(`status check: ${status}`)) +) + +class ServiceError extends Data.TaggedError("ServiceError")<{ + readonly retryable: boolean +}> {} + +let serviceCalls = 0 + +const callService = Effect.gen(function*() { + serviceCalls += 1 + yield* Console.log(`service call ${serviceCalls}`) + + if (serviceCalls < 3) { + return yield* Effect.fail(new ServiceError({ retryable: true })) + } + + return "service response" +}) + +const retryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.recurs(4)) +) + +const program = Effect.gen(function*() { + const finalStatus = yield* readStatus.pipe( + Effect.repeat({ + schedule: Schedule.spaced("10 millis"), + until: (status) => status === "ready" + }) + ) + yield* Console.log(`repeat returned: ${finalStatus}`) + + const response = yield* callService.pipe( + Effect.retry({ + schedule: retryPolicy, + while: (error) => error.retryable + }) + ) + yield* Console.log(`retry returned: ${response}`) +}) + +Effect.runPromise(program) +// Output: +// status check: starting +// status check: starting +// status check: ready +// repeat returned: ready +// service call 1 +// service call 2 +// service call 3 +// retry returned: service response +``` + +`"starting"` is a successful state, so it drives `repeat`. `ServiceError` is a +typed failure, so it drives `retry`. + +##### When not to use each entry point + +Do not choose `repeat` to recover from failures. It propagates the first failure +unless the repeated effect handles that failure itself. + +Do not choose `retry` for ordinary successful states. If `"pending"` or +`"starting"` is a valid response, model it as a success and repeat until it +becomes terminal. + +Do not use either operator to hide unsafe duplication. Keep the repeated or +retried effect scoped to work that is safe to run more than once. + +##### Practical guidance + +Ask these questions in order: + +1. Is the recurrence triggered by success or typed failure? +2. Should the policy inspect the successful value or the error value? +3. Is the first execution part of the external budget? +4. Should the caller receive the effect value or the schedule output? +5. What bound stops the recurrence if the predicate never changes? + +When exhaustion needs recovery, use `Effect.repeatOrElse` for repeated effects +that fail before completion and `Effect.retryOrElse` for retries that exhaust +while the effect is still failing. + +### 3. Minimal Building Blocks + +#### 3.1 Repeat a fixed number of times + +Use `Schedule.recurs(n)` when a successful effect should run once now and then +repeat at most `n` more times. The schedule is the rule for running again; the +effect itself is still executed by `Effect.repeat`. + +##### Problem + +You need a count-only repeat: no delay, predicate, or elapsed-time window. + +##### When to use it + +Use this for small, bounded successful repeats: + +- Running a setup probe a known number of times. +- Taking a fixed number of samples. +- Starting with a count limit before adding spacing or backoff. + +Do not use it when the next run depends on the previous value. In that case, +use `Effect.repeat` options such as `until` or `while`. + +##### Schedule shape + +`Effect.repeat` runs the effect once before the schedule is stepped. Therefore +`Schedule.recurs(4)` means four recurrences after the first run, for five total +executions. + +Beginner note: Recurrence counts — count the first run separately, then use the +schedule for the additional runs. + +| Desired total executions | Policy | +| ------------------------ | -------------------- | +| 1 | `Schedule.recurs(0)` | +| 2 | `Schedule.recurs(1)` | +| 5 | `Schedule.recurs(4)` | + +The `times` option follows the same rule: `Effect.repeat({ times: 4 })` also +means one initial run plus four repeats. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Ref, Schedule } from "effect" + +const program = Effect.gen(function*() { + const runs = yield* Ref.make(0) + + yield* Ref.updateAndGet(runs, (n) => n + 1).pipe( + Effect.tap((run) => Console.log(`run ${run}`)), + Effect.repeat(Schedule.recurs(4)) + ) + + const total = yield* Ref.get(runs) + yield* Console.log(`total runs: ${total}`) +}) + +Effect.runPromise(program) +// Output: +// run 1 +// run 2 +// run 3 +// run 4 +// run 5 +// total runs: 5 +``` + +This prints five runs: the first execution plus four scheduled recurrences. + +##### Variant + +Use `times` when you only need a local fixed repeat and want the final effect +value back: + +```ts runnable deterministic +import { Console, Effect, Ref } from "effect" + +const program = Effect.gen(function*() { + const runs = yield* Ref.make(0) + + const lastValue = yield* Ref.updateAndGet(runs, (n) => n + 1).pipe( + Effect.tap((run) => Console.log(`run ${run}`)), + Effect.repeat({ times: 4 }) + ) + + yield* Console.log(`last value: ${lastValue}`) +}) + +Effect.runPromise(program) +// Output: +// run 1 +// run 2 +// run 3 +// run 4 +// run 5 +// last value: 5 +``` + +With a schedule, `Effect.repeat` succeeds with the schedule output. With +`times`, it succeeds with the last successful value produced by the repeated +effect. + +##### Notes + +The main mistake is counting total executions instead of recurrences. If a +requirement says "run five times total", use `Schedule.recurs(4)` or +`times: 4`. + +#### 3.2 Retry a fixed number of times + +Use `Schedule.recurs(n)` with `Effect.retry` when the whole policy is "retry at +most `n` more times". A retry receives typed failures. These are failures in the +Effect error channel, not defects or interruptions. + +##### Problem + +An effect can fail transiently, and a small immediate retry budget is enough. +There is no delay, backoff, or error-specific filtering yet. + +##### When to use it + +Use this for cheap, idempotent work where retrying immediately is acceptable. +Idempotent means running the operation more than once has the same external +effect as running it once, or the duplicates are safely ignored. + +This is also a useful count limit inside a larger policy that later adds timing. + +##### When not to use it + +Do not use immediate retries against overloaded dependencies, rate-limited APIs, +or slow remote calls. Those usually need spacing, backoff, jitter, or a narrower +error predicate. + +Do not use retry to handle defects or fiber interruptions. `Effect.retry` only +retries typed failures. + +##### Schedule shape + +`Effect.retry` runs the effect once before the schedule is stepped. Each typed +failure is offered to the schedule: + +- `Schedule.recurs(0)` allows no retries. +- `Schedule.recurs(1)` allows one retry, for two attempts total. +- `Schedule.recurs(3)` allows three retries, for four attempts total. + +If a later attempt succeeds, retrying stops immediately. If the schedule stops +while the effect is still failing, the last typed failure is returned. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class RequestError extends Data.TaggedError("RequestError")<{ + readonly attempt: number +}> {} + +let attempt = 0 + +const fetchUser = Effect.gen(function*() { + attempt += 1 + yield* Console.log(`attempt ${attempt}`) + + if (attempt < 4) { + return yield* Effect.fail(new RequestError({ attempt })) + } + + return { id: "user-1", name: "Ada" } +}) + +const program = fetchUser.pipe( + Effect.retry(Schedule.recurs(3)), + Effect.tap((user) => Console.log(`loaded ${user.name}`)) +) + +Effect.runPromise(program) +// Output: +// attempt 1 +// attempt 2 +// attempt 3 +// attempt 4 +// loaded Ada +``` + +The first three attempts fail. The policy permits exactly three retries, so the +fourth attempt can succeed. + +##### Variant + +For one local call site, `Effect.retry({ times: 3 })` has the same retry-count +meaning as `Schedule.recurs(3)`. Prefer the schedule form when you want to name +the policy, pass it around, or compose it with timing. + +##### Notes + +The retry count is not the total attempt count. If an external requirement says +"try three times total", use `Schedule.recurs(2)` or `times: 2`. + +Keep the retry boundary small. Retry the operation that may transiently fail, +not a larger workflow that also performs side effects that should not be +repeated. + +#### 3.3 Add a delay between recurrences + +Use `Schedule.spaced(duration)` when the next recurrence should wait for a +constant delay instead of running immediately. + +##### Problem + +The effect should recur, but a tight loop would be too aggressive. Each +scheduled recurrence needs the same pause. + +##### When to use it + +Use fixed spacing for simple pacing: + +- Polling a resource every few milliseconds or seconds. +- Emitting a heartbeat. +- Adding a small delay between retry attempts. +- Making a count-only example closer to production behavior. + +Do not use an unbounded spaced schedule accidentally. `Schedule.spaced("1 second")` +continues until another condition stops it, so pair it with a count, predicate, +or external interruption when the workflow must be finite. + +Beginner note: Bounds — adding a delay makes a loop slower, not finite. + +##### Schedule shape + +`Schedule.spaced(duration)` keeps recurring and requests the same delay on each +step. With `Effect.repeat`, the first effect execution still happens +immediately; the delay applies before each later recurrence. + +Limit a spaced schedule with `Schedule.take(n)`: + +```ts runnable deterministic +import { Console, Effect, Ref, Schedule } from "effect" + +const program = Effect.gen(function*() { + const runs = yield* Ref.make(0) + + yield* Ref.updateAndGet(runs, (n) => n + 1).pipe( + Effect.tap((run) => Console.log(`run ${run}`)), + Effect.repeat(Schedule.spaced("25 millis").pipe(Schedule.take(3))) + ) + + const total = yield* Ref.get(runs) + yield* Console.log(`total runs: ${total}`) +}) + +Effect.runPromise(program) +// Output: +// run 1 +// run 2 +// run 3 +// run 4 +// total runs: 4 +``` + +This runs four times total: one initial execution plus three spaced +recurrences. + +##### Retry example + +The same schedule can pace retries. In retry, typed failures drive the schedule +instead of successful values. + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class RequestError extends Data.TaggedError("RequestError")<{ + readonly attempt: number +}> {} + +let attempt = 0 + +const request = Effect.gen(function*() { + attempt += 1 + yield* Console.log(`attempt ${attempt}`) + + if (attempt < 3) { + return yield* Effect.fail(new RequestError({ attempt })) + } + + return "ok" +}) + +const program = request.pipe( + Effect.retry(Schedule.spaced("25 millis").pipe(Schedule.take(2))), + Effect.tap((value) => Console.log(`result: ${value}`)) +) + +Effect.runPromise(program) +// Output: +// attempt 1 +// attempt 2 +// attempt 3 +// result: ok +``` + +Here the policy allows two retries and waits 25 milliseconds before each retry. + +##### Notes + +`Schedule.spaced` is a schedule, not a sleep before the first attempt. The first +`repeat` or `retry` attempt is immediate. + +Use `Schedule.addDelay` when you already have a schedule and want to add an +extra computed delay to whatever delay that schedule already chose. The delay +function returns an `Effect`, so any failure or service requirement from that +function becomes part of the schedule. + +#### 3.4 Stop after a limit + +Use a limit whenever a rule for running again must not continue forever. The +limit can be the whole policy, or it can cap another policy such as spacing or +backoff. + +##### Problem + +The recurrence needs a clear stopping rule. + +The common building blocks are: + +- `Schedule.recurs(n)` for a count-only limit. +- `Schedule.take(n)` for limiting another schedule. +- `Schedule.during(duration)` for an elapsed recurrence window. + +Each limit controls recurrences after the initial execution. + +##### When to use it + +Use a limit for retry budgets, finite sampling, bounded tests, and caps on +otherwise unbounded schedules such as `Schedule.spaced("1 second")`. + +Use `Schedule.recurs(n)` when the count is the policy. Use +`schedule.pipe(Schedule.take(n))` when another schedule already describes the +delay or output and only needs a cap. + +##### When not to use it + +Do not use a schedule limit for value-based stopping. If a successful value +decides whether to continue, use `Effect.repeat` with `until` or `while`. If a +typed failure decides whether to retry, use `Effect.retry` with `until` or +`while`. + +Do not use `Schedule.during` as a timeout for a single slow run. A schedule is +consulted between runs; it does not interrupt an effect that is already running. + +Beginner note: Bounds — schedule limits bound future recurrences, not the body +of the current effect. Use an effect timeout when one execution must be +interrupted. + +##### Schedule shape + +`Effect.repeat` and `Effect.retry` run once before stepping the schedule: + +| Limit | Meaning after the first run | +| --------------------------------- | -------------------------------------- | +| `Schedule.recurs(0)` | No additional recurrences | +| `Schedule.recurs(3)` | At most three additional recurrences | +| `schedule.pipe(Schedule.take(3))` | At most three outputs from `schedule` | +| `Schedule.during("30 seconds")` | Recur while the elapsed window is open | + +For retry, "three additional recurrences" means up to three retries. For +repeat, it means up to three additional successful executions. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Ref, Schedule } from "effect" + +const countOnly = Effect.gen(function*() { + const runs = yield* Ref.make(0) + + yield* Ref.updateAndGet(runs, (n) => n + 1).pipe( + Effect.tap((run) => Console.log(`count-only run ${run}`)), + Effect.repeat(Schedule.recurs(2)) + ) + + return yield* Ref.get(runs) +}) + +const spacedAndLimited = Effect.gen(function*() { + const runs = yield* Ref.make(0) + + yield* Ref.updateAndGet(runs, (n) => n + 1).pipe( + Effect.tap((run) => Console.log(`spaced run ${run}`)), + Effect.repeat(Schedule.spaced("20 millis").pipe(Schedule.take(2))) + ) + + return yield* Ref.get(runs) +}) + +const program = Effect.gen(function*() { + const countTotal = yield* countOnly + const spacedTotal = yield* spacedAndLimited + + yield* Console.log(`count-only total: ${countTotal}`) + yield* Console.log(`spaced total: ${spacedTotal}`) +}) + +Effect.runPromise(program) +// Output: +// count-only run 1 +// count-only run 2 +// count-only run 3 +// spaced run 1 +// spaced run 2 +// spaced run 3 +// count-only total: 3 +// spaced total: 3 +``` + +Both policies allow two recurrences after the first run, so both examples run +three times total. + +##### Time window + +Use `Schedule.during` for a best-effort elapsed window, usually with spacing so +the loop does not spin. + +```ts runnable +import { Console, Effect, Ref, Schedule } from "effect" + +const program = Effect.gen(function*() { + const runs = yield* Ref.make(0) + + yield* Ref.updateAndGet(runs, (n) => n + 1).pipe( + Effect.tap((run) => Console.log(`windowed run ${run}`)), + Effect.repeat( + Schedule.spaced("10 millis").pipe( + Schedule.both(Schedule.during("30 millis")) + ) + ) + ) + + const total = yield* Ref.get(runs) + yield* Console.log(`windowed total: ${total}`) +}) + +Effect.runPromise(program) +// Output may vary: elapsed timing can cross the budget boundary differently under load +// windowed run 1 +// windowed run 2 +// windowed run 3 +// windowed run 4 +// windowed total: 4 +``` + +The window is checked at recurrence boundaries. It is not a deadline for the +body of the effect. + +##### Notes + +The off-by-one rule is the main caveat: external requirements often count total +executions, but schedule limits count recurrences after the first execution. If +a requirement says "try three times total", use a limit of `2`. + +#### 3.5 Build intuition before composing policies + +A `Schedule` is a policy value. It receives an input, produces an output, +requests a delay, and decides whether another run is allowed. + +##### What this section is about + +This section is about reading one schedule before combining it with another. +A schedule is not the effect being repeated or retried. It is stepped after +`Effect.repeat` sees a success or after `Effect.retry` sees a typed failure. + +Once that model is clear, composition is easier: combined schedules are still +made from inputs, outputs, delays, and stop conditions. + +##### Four questions + +Ask these questions for any schedule: + +- What input does it observe? +- What output does it produce? +- What delay does it request? +- When does it stop? + +For `Schedule.recurs(n)`, the important axis is stopping. It permits `n` +recurrences after the first execution and outputs the zero-based recurrence +count. + +For `Schedule.spaced(duration)`, the important axis is delay. It keeps recurring +and asks for the same delay between completed runs. + +For `Schedule.fixed(interval)`, the important axis is clock alignment. It aims +at regular interval boundaries instead of simply waiting a fixed pause after +each run. + +For `Schedule.exponential(base, factor)` and `Schedule.fibonacci(one)`, the +important axis is delay growth. They keep recurring until another policy or +entry-point condition stops them. + +##### Common mistakes + +Do not treat "repeat three times" and "run three times total" as the same +requirement. `Effect.repeat` and `Effect.retry` run once before the schedule +controls additional recurrences. + +Do not assume timing policies are bounded. `Schedule.spaced`, +`Schedule.fixed`, `Schedule.exponential`, `Schedule.fibonacci`, and +`Schedule.forever` can continue indefinitely unless another condition stops +them. + +Do not assume all delays mean the same thing. A spaced policy waits between +runs, a fixed policy aims at interval boundaries, and a backoff policy changes +the delay from step to step. + +##### Practical guidance + +Before composing policies, describe each one in a short sentence: + +- A count policy says how many recurrences are allowed after the first run. +- A timing policy says whether the delay is spacing, clock alignment, or growth. +- An output-producing policy says whether the useful value is a count, duration, + input, or transformed value. + +Prefer the smallest schedule that states the behavior you need now. Add +composition only when there is a second policy to express, such as "retry three +times and wait between attempts." + +## Part II — Retry Recipes + +### 4. Retry Limits and Simple Delays + +#### 4.1 Retry up to 3 times + +Use `Effect.retry({ times: 3 })` when a typed failure should get up to three +more attempts before the final failure is returned. + +##### Problem + +The operation may fail briefly, and immediate retry is acceptable. The original +attempt runs once; the policy allows up to three retries after that. + +##### When to use it + +Use this for cheap, idempotent work where a short burst is useful: a local +resource conflict, a dependency warming up, or a read that can fail during a +brief restart. + +Use a delay or backoff instead when retrying immediately would increase pressure +on a remote or overloaded dependency. + +##### Schedule shape + +The options form is the smallest expression: + +| Policy | Maximum total executions | +| ---------------------------- | ------------------------ | +| `Effect.retry({ times: 0 })` | 1 | +| `Effect.retry({ times: 1 })` | 2 | +| `Effect.retry({ times: 3 })` | 4 | + +`Schedule.recurs(3)` has the same retry-count meaning when used with +`Effect.retry`. + +Beginner note: Recurrence counts — retry budgets count follow-up attempts, not +the original attempt. + +If an attempt succeeds, retrying stops immediately. If every permitted attempt +fails, `Effect.retry` returns the last typed failure. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect } from "effect" + +class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{ + readonly attempt: number +}> {} + +let attempt = 0 + +const callService = Effect.gen(function*() { + attempt += 1 + yield* Console.log(`attempt ${attempt}`) + + if (attempt < 4) { + return yield* Effect.fail(new ServiceUnavailable({ attempt })) + } + + return "service response" +}) + +const program = callService.pipe( + Effect.retry({ times: 3 }), + Effect.tap((response) => Console.log(`completed: ${response}`)) +) + +Effect.runPromise(program) +// Output: +// attempt 1 +// attempt 2 +// attempt 3 +// attempt 4 +// completed: service response +``` + +Attempts 1, 2, and 3 fail. Attempt 4 is the third retry, so it is still inside +the budget and can succeed. + +##### Variants + +Use `Schedule.recurs(3)` when the retry policy should be named or composed with +timing later. Use `Effect.retry(callService, Schedule.recurs(3))` when the +two-argument style reads better at the call site. + +Add `while` or `until` when only some typed failures should be retried. The +retry count still caps the number of retries. + +##### Notes + +The first execution is not counted as a retry. If a requirement says "try this +at most three times total", use `times: 2` or `Schedule.recurs(2)`. + +`Effect.retry` retries typed failures from the error channel. It does not retry +defects or interruptions. + +#### 4.2 Retry with a small constant delay + +Combine `Schedule.spaced(duration)` with a count limit when immediate retries +are too aggressive but full backoff is unnecessary. + +##### Problem + +The retry policy needs two constraints: wait a fixed amount before each retry, +and stop after a small number of retries. + +##### When to use it + +Use this for short-lived failures where a tiny pause helps: local service +startup, brief lock contention, or an idempotent request to a dependency that +usually recovers quickly. + +Do not use a constant delay as the default for overloaded or rate-limited +systems. Those usually need backoff, jitter, or error-specific handling. + +##### Schedule shape + +`Schedule.spaced(duration)` keeps recurring with the same delay. `Schedule.recurs(n)` +caps the retry count. `Schedule.both` combines them with intersection semantics: +both schedules must continue, and the combined delay is the maximum of their +delays. + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class TemporaryRequestError extends Data.TaggedError("TemporaryRequestError")<{ + readonly attempt: number +}> {} + +let attempt = 0 + +const request = Effect.gen(function*() { + attempt += 1 + yield* Console.log(`attempt ${attempt}`) + + if (attempt < 4) { + return yield* Effect.fail(new TemporaryRequestError({ attempt })) + } + + return { id: "user-1", name: "Ada" } +}) + +const retryPolicy = Schedule.spaced("25 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +const program = request.pipe( + Effect.retry(retryPolicy), + Effect.tap((user) => Console.log(`loaded ${user.name}`)) +) + +Effect.runPromise(program) +// Output: +// attempt 1 +// attempt 2 +// attempt 3 +// attempt 4 +// loaded Ada +``` + +The policy allows three retries and waits 25 milliseconds before each retry. +The first execution is not delayed. + +##### Variants + +For a local policy, `Effect.retry({ schedule: Schedule.spaced("25 millis"), times: 3 })` +expresses the same count and delay. Use the explicit schedule composition when +you want to name, reuse, or extend the policy. + +Changing the duration changes only the pause between attempts; the retry count +is still controlled by `Schedule.recurs(3)`. + +##### Notes + +`Effect.retry` stops at the first success. The combined schedule output is not +the final success value; it only controls whether and when another retry should +happen. + +Keep the retried effect small and safe to run more than once. + +#### 4.3 Retry immediately, but only briefly + +Use `Schedule.recurs(n)` when a failure is likely to disappear right away and +only a small retry burst is acceptable. + +##### Problem + +The policy should retry without delay, but it still needs a hard cap. The count +limit prevents an immediate retry loop from continuing indefinitely. + +##### When to use it + +Use this when the operation is cheap, safe to repeat, and likely failing because +of a short local race or momentary unavailability. One or two immediate retries +is often enough. + +Do not use this against dependencies that may be overloaded. Remote calls, +database reconnects, queue consumers, and rate-limited APIs usually need delay +or backoff. + +##### Schedule shape + +`Schedule.recurs(times)` ignores its input and outputs a zero-based recurrence +count. With `Effect.retry`, the input is the typed error from the failed +attempt, but the successful result is still the value produced by the retried +effect. + +The count is a retry count: + +- `Schedule.recurs(0)` allows no retries. +- `Schedule.recurs(1)` allows one retry. +- `Schedule.recurs(2)` allows two retries. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Ref, Schedule } from "effect" + +class CacheBusy extends Data.TaggedError("CacheBusy")<{ + readonly attempt: number +}> {} + +const readSnapshot = Effect.fnUntraced(function*(attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`attempt ${attempt}`) + + if (attempt <= 2) { + return yield* Effect.fail(new CacheBusy({ attempt })) + } + + return { version: "v1", entries: 42 } +}) + +const retryBriefly = Schedule.recurs(2) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + + const snapshot = yield* readSnapshot(attempts).pipe( + Effect.retry(retryBriefly) + ) + + yield* Console.log(`snapshot ${snapshot.version}: ${snapshot.entries} entries`) +}) + +Effect.runPromise(program) +// Output: +// attempt 1 +// attempt 2 +// attempt 3 +// snapshot v1: 42 entries +``` + +The first two attempts fail, and the second retry succeeds. If the third attempt +failed too, `Effect.retry` would return that final `CacheBusy` failure. + +##### Variants + +For a local policy, `Effect.retry({ times: 2 })` has the same retry-count +meaning. Keep `Schedule.recurs(2)` when the policy should be named, shared, or +combined with timing later. + +##### Notes + +The first attempt always runs. If it succeeds, no retry happens. + +This recipe deliberately avoids delay, backoff, and jitter. Once the operation +crosses a process, network, or rate-limit boundary, use a paced retry policy. + +#### 4.4 Retry until the first success + +`Effect.retry` stops as soon as one attempt succeeds. The schedule is only +consulted after typed failures. + +##### Problem + +The operation may fail a few times, but the first success should complete the +whole workflow and leave any remaining retry budget unused. + +##### When to use it + +Use this when success means the work is done: connecting to a service, reading a +temporarily unavailable value, or retrying an idempotent request after transient +typed failures. + +Use `Effect.repeat` instead when successful values should drive more executions. + +##### Schedule shape + +For `Effect.retry`, each typed failure is offered to the schedule: + +- If the schedule continues, the effect is run again. +- If the schedule stops, the last typed failure is returned. +- If the next attempt succeeds, the whole retried effect succeeds immediately. + +`Schedule.recurs(4)` permits up to five total executions, but fewer executions +happen when an earlier attempt succeeds. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Ref, Schedule } from "effect" + +class TemporaryError extends Data.TaggedError("TemporaryError")<{ + readonly attempt: number +}> {} + +const flakyRequest = Effect.fnUntraced(function*(attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`attempt ${attempt}`) + + if (attempt < 3) { + return yield* Effect.fail(new TemporaryError({ attempt })) + } + + return `success on attempt ${attempt}` +}) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + + const value = yield* flakyRequest(attempts).pipe( + Effect.retry(Schedule.recurs(4)) + ) + + const totalAttempts = yield* Ref.get(attempts) + yield* Console.log(`${value}; total attempts: ${totalAttempts}`) +}) + +Effect.runPromise(program) +// Output: +// attempt 1 +// attempt 2 +// attempt 3 +// success on attempt 3; total attempts: 3 +``` + +The schedule allows four retries, but attempts 4 and 5 never run because +attempt 3 succeeds. + +##### Variants + +`Effect.retry({ times: 4 })` has the same count-only behavior. Add a schedule, +such as `Schedule.spaced("200 millis").pipe(Schedule.take(4))`, when failures +should be paced. Add `while` or `until` when only some typed failures should be +retried. + +The first success still wins. Predicates and schedules only decide what happens +after failures. + +##### Notes + +Plain `Effect.retry` does not run a fallback when the policy is exhausted. Use +`Effect.retryOrElse` when final failure should trigger recovery. + +Prefer a bounded or otherwise controlled schedule when unbounded retry is risky. +For production dependencies, that usually means combining a retry count with a +delay or backoff policy. + +#### 4.5 Retry with a delay suitable for external APIs + +For simple external API calls, combine a modest fixed delay with a retry limit +and an error predicate. The schedule answers "when"; the predicate answers +"whether this failure is safe to retry." + +##### Problem + +External APIs can fail transiently at the network or service boundary, but +retrying every failure can hammer the provider or repeat unsafe requests. + +##### When to use it + +Use this for idempotent external API calls where a short constant pause is +acceptable: reads, metadata lookups, status checks, or writes protected by an +idempotency key. + +A one-second delay with a small retry budget is a readable default when the API +does not publish a more specific retry policy. + +##### When not to use it + +Do not retry client errors such as invalid input, authentication failure, +authorization failure, or most not-found responses. Those usually need to be +returned or handled directly. + +Do not ignore provider guidance. If the API returns `Retry-After`, exposes +rate-limit reset metadata, or documents endpoint-specific retry rules, model +that policy instead of using a fixed delay. + +##### Schedule shape + +The options form keeps the three policy pieces together: + +- `schedule: Schedule.spaced("1 second")` waits one second before each retry +- `times: 4` permits four retries after the original attempt +- `while: isRetryableApiError` retries only selected typed failures + +If an attempt succeeds, retrying stops immediately. If a non-retryable error is +returned, the retry budget is not spent. + +##### Example + +```ts +import { Console, Data, Effect, Fiber, Ref, Schedule } from "effect" +import { TestClock } from "effect/testing" + +class ExternalApiError extends Data.TaggedError("ExternalApiError")<{ + readonly attempt: number + readonly status: number +}> {} + +interface Customer { + readonly id: string + readonly name: string +} + +const fetchCustomer = Effect.fnUntraced(function*( + id: string, + attempts: Ref.Ref +) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`api attempt ${attempt}`) + + if (attempt < 3) { + return yield* Effect.fail(new ExternalApiError({ attempt, status: 503 })) + } + + return { id, name: "Ada" } satisfies Customer +}) + +const isRetryableApiError = (error: ExternalApiError) => + error.status === 408 || + error.status === 429 || + error.status >= 500 + +const retryExternalApi = { + schedule: Schedule.spaced("1 second"), + times: 4, + while: isRetryableApiError +} + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const fiber = yield* fetchCustomer("customer-123", attempts).pipe( + Effect.retry(retryExternalApi), + Effect.forkScoped + ) + + yield* TestClock.adjust("1 second") + yield* TestClock.adjust("1 second") + + const customer = yield* Fiber.join(fiber) + yield* Console.log(`customer: ${customer.id} ${customer.name}`) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program).then(() => undefined) +``` + +The API fails twice with a retryable `503`, waits one virtual second before +each retry, and then returns the customer. + +##### Notes + +`times: 4` means four retries after the original attempt, so the API can be +called at most five times. If a provider says "four total attempts", use +`times: 3`. + +A fixed delay is intentionally simple. It does not inspect headers, adapt to +congestion, add jitter, or cap long-running retry behavior. + +Keep the retry boundary around the single idempotent API call. Avoid wrapping +local writes, notifications, or other effects that should not run more than +once. + +#### 4.6 Retry with different fixed delays for different environments + +Keep the retry shape stable and select only the delay from configuration. The +operation, retry budget, and retryability rules should not drift just because +the program is running locally, in staging, or in production. + +##### Problem + +Development often benefits from shorter retry delays, while production should +avoid fast retry pressure. You need the environment to choose the fixed delay +without changing the rest of the policy. + +##### When to use it + +Use this when the operation is safe to retry and timing is the only +environment-specific difference. It fits idempotent service calls, reconnects, +and dependency probes where local responsiveness and production restraint are +both useful. + +##### When not to use it + +Do not use environment-specific delays to hide a different policy. If +production needs fewer retries, stricter error filtering, backoff, jitter, or a +fallback path, model that explicitly. + +Do not make a non-idempotent operation safe by changing the delay. Duplicate +side effects still need a domain-level guarantee such as an idempotency key. + +##### Schedule shape + +The environment selects a `Duration.Input`, and `Schedule.spaced(delay)` uses +that same delay before each retry. + +Combining it with `Schedule.recurs(3)` keeps the shape bounded: one original +attempt, then at most three retries. `Schedule.both` requires both schedules to +continue; the spaced schedule supplies the delay, and the recurrence schedule +supplies the limit. + +##### Example + +```ts +import { Console, Data, Duration, Effect, Fiber, Ref, Schedule } from "effect" +import { TestClock } from "effect/testing" + +type Environment = "development" | "staging" | "production" + +class RequestError extends Data.TaggedError("RequestError")<{ + readonly attempt: number +}> {} + +const request = Effect.fnUntraced(function*(attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`request attempt ${attempt}`) + + if (attempt < 3) { + return yield* Effect.fail(new RequestError({ attempt })) + } + + return "accepted" +}) + +const retryDelays: Record = { + development: "50 millis", + staging: "250 millis", + production: "1 second" +} + +const retryPolicy = (environment: Environment) => + Schedule.spaced(retryDelays[environment]).pipe( + Schedule.both(Schedule.recurs(3)) + ) + +const runRequest = Effect.fnUntraced(function*( + environment: Environment, + attempts: Ref.Ref +) { + return yield* request(attempts).pipe( + Effect.retry(retryPolicy(environment)) + ) +}) + +const program = Effect.gen(function*() { + const environment: Environment = "production" + const attempts = yield* Ref.make(0) + const fiber = yield* runRequest(environment, attempts).pipe(Effect.forkScoped) + + yield* TestClock.adjust(retryDelays[environment]) + yield* TestClock.adjust(retryDelays[environment]) + + const result = yield* Fiber.join(fiber) + yield* Console.log(`result in ${environment}: ${result}`) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program).then(() => undefined) +``` + +The example uses the production delay, so each retry waits one virtual second. +Changing `environment` to `"development"` keeps the same retry limit and uses +50 milliseconds instead. + +##### Notes + +`Schedule.spaced` is the usual fixed-delay constructor for retry policies. It +waits after a failure before the next attempt. `Schedule.fixed` is for +maintaining a recurring wall-clock cadence and is not the right default here. + +The environment is selected when the policy is built. If configuration can +change at runtime, rebuild the policy at the boundary where `Effect.retry` is +called. + +`Effect.retry` retries typed failures from the error channel. Defects and fiber +interruptions are not retried as typed failures. + +### 5. Exponential and Capped Backoff + +#### 5.1 Basic exponential backoff + +`Schedule.exponential(base)` starts with `base` as the first retry delay and +multiplies later delays by the factor, which defaults to `2`. + +##### Problem + +A dependency may be unhealthy long enough that fixed-delay retries keep adding +pressure. You want early recovery to be quick, but repeated failures should +make the caller slow down. + +##### When to use it + +Use exponential backoff for idempotent operations whose failures are probably +temporary: network calls, brief service unavailability, short database +failovers, or dependency probes. + +It is a better remote-call default than a tight loop because each failed retry +decision increases the pause before the next attempt. + +##### When not to use it + +Do not use backoff for operations that are unsafe to run more than once. +Retried writes need idempotency, deduplication, transactions, or another +domain-specific guarantee. + +Do not leave the schedule unbounded unless retrying forever is intentional and +the fiber is supervised. Basic exponential backoff also has no jitter, so many +callers that fail together can still retry together. + +##### Schedule shape + +`Schedule.exponential("100 millis")` produces these retry delays with the +default factor: + +- first retry: 100 milliseconds +- second retry: 200 milliseconds +- third retry: 400 milliseconds +- fourth retry: 800 milliseconds + +With `Effect.retry`, the original attempt is immediate. The schedule is +consulted only after a typed failure. Pair it with `Schedule.recurs(5)` or +`times: 5` when the caller needs a final failure after five retries. + +##### Example + +```ts +import { Console, Data, Effect, Fiber, Ref, Schedule } from "effect" +import { TestClock } from "effect/testing" + +class RequestError extends Data.TaggedError("RequestError")<{ + readonly attempt: number +}> {} + +const fetchUser = Effect.fnUntraced(function*(id: string, attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`fetch user attempt ${attempt}`) + + if (attempt < 4) { + return yield* Effect.fail(new RequestError({ attempt })) + } + + return { id, name: "Ada" } +}) + +const retryWithBackoff = Schedule.exponential("100 millis").pipe( + Schedule.both(Schedule.recurs(5)) +) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const fiber = yield* fetchUser("user-123", attempts).pipe( + Effect.retry(retryWithBackoff), + Effect.forkScoped + ) + + yield* TestClock.adjust("100 millis") + yield* TestClock.adjust("200 millis") + yield* TestClock.adjust("400 millis") + + const user = yield* Fiber.join(fiber) + yield* Console.log(`loaded user: ${user.name}`) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program).then(() => undefined) +``` + +The example fails three times, then succeeds on the fourth attempt. The virtual +clock advances through the 100, 200, and 400 millisecond backoff delays, so the +snippet terminates immediately. + +##### Notes + +The first execution is not delayed. Backoff begins only after the effect fails +with a typed error. + +`Schedule.recurs(5)` means five retries after the original attempt, so the +effect can run up to six times. + +`Schedule.exponential` outputs the current delay. After `Schedule.both`, the +combined output is a tuple of the exponential delay and the recurrence count. +Plain `Effect.retry` uses that output for scheduling and returns the successful +value of the retried effect. + +#### 5.2 Backoff for transient network failures + +Network failures are often temporary, but repeated retry attempts should slow +down. Use exponential backoff with a finite retry budget and retry only the +typed failures that are plausibly transient. + +##### Problem + +Remote calls can fail because of connection resets, timeouts, temporary DNS +failures, or gateway errors. Retrying immediately can turn a small transport +problem into extra load on both the service and the client. + +##### When to use it + +Use this for idempotent network calls: reads, status checks, reconnects, and +writes protected by an idempotency key. + +It is useful when the request itself is valid and the failure is about the +transport path or temporary gateway behavior. A timeout may succeed later; an +invalid request usually will not. + +##### When not to use it + +Do not retry permanent request problems such as invalid input, authentication +failure, authorization failure, or response decoding failures. + +Do not ignore server guidance. If an API returns `Retry-After` or explicit +rate-limit metadata, prefer a policy that honors it. + +##### Schedule shape + +`Schedule.exponential("100 millis")` starts at 100 milliseconds and doubles +after each failed retry decision. `Schedule.recurs(5)` limits the policy to +five retries after the original attempt. + +In the options form, `while` filters the typed error before spending another +retry. If the predicate returns `false`, `Effect.retry` fails with that error +immediately. + +##### Example + +```ts +import { Console, Data, Effect, Fiber, Ref, Schedule } from "effect" +import { TestClock } from "effect/testing" + +class NetworkFailure extends Data.TaggedError("NetworkFailure")<{ + readonly reason: "ConnectionReset" | "Timeout" | "TemporaryDnsFailure" +}> {} + +class HttpFailure extends Data.TaggedError("HttpFailure")<{ + readonly status: number +}> {} + +class DecodeFailure extends Data.TaggedError("DecodeFailure")<{ + readonly message: string +}> {} + +type FetchUserError = NetworkFailure | HttpFailure | DecodeFailure + +const fetchUser = Effect.fnUntraced(function*(id: string, attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`network attempt ${attempt}`) + + if (attempt === 1) { + return yield* Effect.fail(new NetworkFailure({ reason: "Timeout" })) + } + if (attempt === 2) { + return yield* Effect.fail(new HttpFailure({ status: 502 })) + } + + return { id, name: "Ada" } +}) + +const isRetryableNetworkFailure = (error: FetchUserError): boolean => { + switch (error._tag) { + case "NetworkFailure": + return true + case "HttpFailure": + return error.status === 408 || error.status === 502 || error.status === 504 + case "DecodeFailure": + return false + } +} + +const networkBackoff = Schedule.exponential("100 millis").pipe( + Schedule.both(Schedule.recurs(5)) +) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const fiber = yield* fetchUser("user-123", attempts).pipe( + Effect.retry({ + schedule: networkBackoff, + while: isRetryableNetworkFailure + }), + Effect.forkScoped + ) + + yield* TestClock.adjust("100 millis") + yield* TestClock.adjust("200 millis") + + const user = yield* Fiber.join(fiber) + yield* Console.log(`loaded user: ${user.name}`) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program).then(() => undefined) +``` + +The first failure is a timeout, the second is a retryable gateway failure, and +the third attempt succeeds. A `DecodeFailure` would stop immediately because +the predicate returns `false`. + +##### Notes + +The first request is not delayed. Backoff begins only after the effect fails +with a typed error. + +`Schedule.exponential` is unbounded by itself. Pair it with `Schedule.recurs`, +`times`, a deadline, or another stopping condition unless unbounded retry is +intentional. + +For many concurrent callers, add jitter later so callers do not all retry on +the same exponential intervals. + +#### 5.3 Backoff for overloaded downstream services + +When a dependency reports overload, each failed retry should reduce this +caller's pressure on that dependency. Exponential backoff gives that behavior +for one call site. + +##### Problem + +A downstream service may return overload errors, reject requests, or fail +because a pool is saturated. Retrying at a fixed rate keeps adding traffic +while the dependency is least able to handle it. + +##### When to use it + +Use this when the failure is a typed retryable overload signal, such as `503 +Service Unavailable`, `429 Too Many Requests`, queue saturation, or short-lived +connection pool exhaustion. + +The retried operation must be idempotent or otherwise duplicate-safe. + +##### When not to use it + +Do not use backoff to hide permanent failures such as invalid input, missing +authorization, or a request shape the downstream will never accept. + +Do not treat per-request backoff as the whole overload strategy for a busy +client. If many fibers can call the same service concurrently, also consider +admission control such as queues, rate limits, or concurrency limits. + +##### Schedule shape + +`Schedule.exponential("100 millis")` yields retry delays of 100 milliseconds, +200 milliseconds, 400 milliseconds, 800 milliseconds, and so on. Combining it +with `Schedule.recurs(5)` permits at most five retries after the original +attempt. + +`Schedule.both` requires both schedules to continue. The exponential schedule +contributes the growing delay, and the recurrence schedule contributes the +limit. + +##### Example + +```ts +import { Console, Data, Effect, Fiber, Ref, Schedule } from "effect" +import { TestClock } from "effect/testing" + +class DownstreamOverloaded extends Data.TaggedError("DownstreamOverloaded")<{ + readonly service: string + readonly attempt: number +}> {} + +const callInventory = Effect.fnUntraced(function*(attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`inventory attempt ${attempt}`) + + if (attempt < 4) { + return yield* Effect.fail( + new DownstreamOverloaded({ service: "inventory", attempt }) + ) + } + + return { sku: "sku-123", available: true } +}) + +const overloadBackoff = Schedule.exponential("100 millis").pipe( + Schedule.both(Schedule.recurs(5)) +) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const fiber = yield* callInventory(attempts).pipe( + Effect.retry(overloadBackoff), + Effect.forkScoped + ) + + yield* TestClock.adjust("100 millis") + yield* TestClock.adjust("200 millis") + yield* TestClock.adjust("400 millis") + + const result = yield* Fiber.join(fiber) + yield* Console.log(`available: ${result.available}`) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program).then(() => undefined) +``` + +The first three calls fail with `DownstreamOverloaded`. The retry delays grow +from 100 to 200 to 400 milliseconds, then the fourth call succeeds. + +##### Notes + +Backoff only affects retry attempts after typed failures. It does not delay the +original request. + +Keep the retried effect narrow. Retry the downstream request itself, not a +larger workflow that may already have performed local writes or sent +notifications. + +In high fan-out clients, add a cap and jitter so many callers do not retry at +the same growing intervals. + +#### 5.4 Backoff for startup dependency readiness + +Startup often races with nearby services. A bounded exponential retry can wait +for a dependency to become ready without turning startup into an endless loop. + +##### Problem + +A database may accept connections a few seconds after the app process starts, +or a local cache may still be warming. The app should wait briefly, with +increasing pauses, and then fail startup clearly if readiness never arrives. + +##### When to use it + +Use this for idempotent readiness checks: opening a connection, pinging a +service, or verifying that a required endpoint accepts requests. + +It fits local development, tests, containers, and deployments where process +ordering is not the same thing as dependency readiness. + +##### When not to use it + +Do not retry misconfiguration. Bad credentials, invalid host names, missing +schemas, and authorization errors should usually fail startup immediately. + +Do not wrap non-idempotent setup work in this policy. Migrations, table +creation, message publication, and external registration need their own +duplicate-safe design before they can be retried. + +##### Schedule shape + +`Schedule.exponential("200 millis")` produces 200 millisecond, 400 +millisecond, 800 millisecond, and 1.6 second delays with the default factor. + +`Schedule.recurs(8)` allows eight retries after the original readiness check. +Combined with `Schedule.both`, the exponential schedule supplies the delay and +the recurrence schedule stops the policy after the retry budget is exhausted. + +##### Example + +```ts +import { Console, Data, Effect, Fiber, Ref, Schedule } from "effect" +import { TestClock } from "effect/testing" + +class DependencyNotReady extends Data.TaggedError("DependencyNotReady")<{ + readonly dependency: string + readonly attempt: number +}> {} + +const waitForDatabase = Effect.fnUntraced(function*(attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`database readiness attempt ${attempt}`) + + if (attempt < 4) { + return yield* Effect.fail( + new DependencyNotReady({ dependency: "database", attempt }) + ) + } +}) + +const startApplication = Console.log("application started") + +const startupDependencyBackoff = Schedule.exponential("200 millis").pipe( + Schedule.both(Schedule.recurs(8)) +) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const fiber = yield* Effect.gen(function*() { + yield* waitForDatabase(attempts).pipe( + Effect.retry(startupDependencyBackoff) + ) + yield* startApplication + }).pipe(Effect.forkScoped) + + yield* TestClock.adjust("200 millis") + yield* TestClock.adjust("400 millis") + yield* TestClock.adjust("800 millis") + + yield* Fiber.join(fiber) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program).then(() => undefined) +``` + +The readiness check fails three times, backs off through 200, 400, and 800 +milliseconds, then starts the application. + +##### Notes + +The first readiness check runs immediately. Only retry attempts are delayed. + +`Schedule.recurs(8)` means eight retries after the original attempt, so this +policy allows up to nine readiness checks. + +Retry the readiness check itself, not the entire startup workflow. Initialization +steps that already succeeded should not be run again because a later dependency +probe failed. + +#### 5.5 Backoff with a practical base interval + +The `base` passed to `Schedule.exponential(base, factor?)` is the first retry +delay. Choose it from the operation's real latency and recovery expectations, +not from the later delays you eventually want. + +##### Problem + +A base interval that is too small behaves like immediate retry for the first +few failures. A base interval that is too large can make recoverable +user-facing failures feel slow. + +##### When to use it + +Use this when retries should start soon, but repeated failures should become +less frequent. A base of a few hundred milliseconds is often practical for +idempotent remote calls where immediate retry is too aggressive and a +multi-second first retry is too slow. + +It is also useful when moving from fixed delays to backoff: keep the first +retry near the fixed delay that already worked, then let the exponential shape +reduce pressure if failures continue. + +##### When not to use it + +Do not use tiny base intervals such as 1 millisecond for remote dependencies. +They can add load before the dependency has had time to recover. + +Do not make the base large only because later retries need to be far apart. If +the first retry should be quick but later retries need a ceiling, add a cap as +a separate policy choice. + +##### Schedule shape + +`Schedule.exponential(base, factor?)` always recurs and returns the current +delay. The default factor is `2`, so `Schedule.exponential("500 millis")` +produces approximately 500 milliseconds, 1 second, 2 seconds, and 4 seconds. + +With `Effect.retry`, the original attempt runs immediately. The base interval +is the first pause after a typed failure. + +##### Example + +```ts +import { Console, Data, Effect, Fiber, Ref, Schedule } from "effect" +import { TestClock } from "effect/testing" + +class ServiceError extends Data.TaggedError("ServiceError")<{ + readonly attempt: number + readonly status: number +}> {} + +const loadAccount = Effect.fnUntraced(function*(id: string, attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`account attempt ${attempt}`) + + if (attempt < 4) { + return yield* Effect.fail(new ServiceError({ attempt, status: 503 })) + } + + return { id, balance: 100 } +}) + +const isRetryableServiceError = (error: ServiceError) => + error.status === 408 || + error.status === 429 || + error.status >= 500 + +const retryWithPracticalBackoff = { + schedule: Schedule.exponential("500 millis"), + times: 4, + while: isRetryableServiceError +} + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const fiber = yield* loadAccount("account-123", attempts).pipe( + Effect.retry(retryWithPracticalBackoff), + Effect.forkScoped + ) + + yield* TestClock.adjust("500 millis") + yield* TestClock.adjust("1 second") + yield* TestClock.adjust("2 seconds") + + const account = yield* Fiber.join(fiber) + yield* Console.log(`balance: ${account.balance}`) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program).then(() => undefined) +``` + +The first retry waits 500 milliseconds, then the next retries wait about 1 +second and 2 seconds. The example advances virtual time so the full backoff +shape runs immediately. + +##### Notes + +`Schedule.exponential(base, factor?)` does not stop on its own. For +request/response work, combine it with `times`, `Schedule.recurs`, a predicate, +or another stopping condition. + +Choose the base from the operation's timing. Local coordination may only need +tens of milliseconds. External APIs often need a few hundred milliseconds or a +full second. Slow recovery paths should start larger. + +Caps and jitter are common production refinements, especially for large fleets +or rate-limited services, but they are separate choices from the base interval. + +#### 5.6 Exponential backoff with a maximum delay + +Use capped exponential backoff when early retries should spread out quickly, but +no single wait should exceed a known maximum. + +##### Problem + +Plain `Schedule.exponential` keeps growing. That is useful at first, but later +delays can exceed the request budget, worker lease, or supervisor timeout. + +Cap the delay by combining exponential backoff with `Schedule.spaced(maxDelay)` +using `Schedule.either`. `either` continues while either schedule continues and +uses the smaller delay. Add `Schedule.recurs(n)` with `Schedule.both` when the +policy also needs a retry limit. + +##### When to use it + +Use this shape for transient failures in idempotent calls: external APIs, +databases, queues, caches, and service clients. The first retries happen soon, +then the delay settles at the cap instead of growing without bound. + +The cap is a per-retry maximum. It is not a total timeout and does not interrupt +an attempt that is already running. + +##### When not to use it + +Do not retry operations that are unsafe to run more than once unless the call is +made idempotent with a key, transaction, de-duplication, or another domain +guarantee. + +Do not use capped backoff alone for high fan-out clients. If many callers can +fail together, combine the policy with jitter, admission control, or rate +limits. + +##### Schedule shape + +With a base of 10 milliseconds and a cap of 40 milliseconds, the delay sequence +is 10 milliseconds, 20 milliseconds, 40 milliseconds, 40 milliseconds, and so +on. The exponential side wants to continue forever, and the spaced side also +wants to continue forever, so `Schedule.recurs(n)` supplies the stopping point. + +`Schedule.both(Schedule.recurs(n))` keeps the capped delay because `recurs` +adds no meaningful wait. It only contributes the retry budget. `Schedule.recurs(4)` +means four retries after the original attempt, so the effect can run up to five +times total. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class ApiError extends Data.TaggedError("ApiError")<{ + readonly status: number +}> {} + +let attempts = 0 + +const request = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`request attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail(new ApiError({ status: 503 })) + } + + return "response body" +}) + +const cappedBackoff = Schedule.exponential("10 millis").pipe( + Schedule.either(Schedule.spaced("40 millis")), + Schedule.both(Schedule.recurs(4)) +) + +const program = request.pipe( + Effect.retry(cappedBackoff), + Effect.tap((body) => Console.log(`success: ${body}`)) +) + +Effect.runPromise(program).then(() => undefined, console.error) +// Output: +// request attempt 1 +// request attempt 2 +// request attempt 3 +// request attempt 4 +// success: response body +``` + +The first call is immediate. If it fails with a typed `ApiError`, the next waits +10 milliseconds. Later failures wait 20 milliseconds, then 40 milliseconds, and +the cap prevents longer waits. If all four retries fail, `Effect.retry` +propagates the last `ApiError`. + +##### Variants + +For interactive work, use a small base, a small cap, and a short retry budget, +for example a 50 millisecond base capped at 1 second with three to five +retries. + +For background work, use a larger base and cap, such as 500 milliseconds capped +at 30 seconds, but still keep an explicit retry count unless retrying forever is +intentional. + +When only some typed failures are retryable, keep the capped schedule and pass +`Effect.retry({ schedule, while })`. The `while` predicate decides which errors +may consume retry budget; the schedule still decides timing and count. + +##### Notes and caveats + +There is no dedicated cap constructor in this recipe. The cap comes from +`Schedule.either(Schedule.spaced(maxDelay))`. + +Do not replace `either` with `both` for the cap. `Schedule.both` uses the larger +delay, so pairing exponential backoff directly with fixed spacing would wait at +least the fixed duration from the first retry. + +The composed schedule output is nested composition data. Plain `Effect.retry` +uses it for timing and stopping, then returns the successful value produced by +the retried effect. + +#### 5.7 Preventing excessively long waits + +Use a capped schedule when exponential growth is useful, but very long waits are +not useful to the caller. + +##### Problem + +After enough failures, exponential backoff can wait longer than the operation is +worth. The caller may need a failure result, the job may need to release its +lease, or an operator may expect the workflow to stop within a known window. + +Use `Schedule.either` with a fixed `Schedule.spaced` schedule to cap each delay, +then add a separate retry limit. The cap and retry count solve different +problems and usually belong together. + +##### When to use it + +Use this policy for idempotent calls to services that may be overloaded, +rate-limited, restarting, or briefly unavailable. Short early waits absorb +small interruptions. The cap prevents later waits from becoming operationally +surprising. + +Choose the cap from the caller's budget, not from the downstream service alone. +A web request, queue job, and supervisor loop often need different caps for the +same dependency. + +##### When not to use it + +Do not retry permanent failures such as invalid input, missing authorization, or +a request the downstream will never accept. Those should fail fast or be handled +by domain logic. + +Do not treat the delay cap as an attempt timeout. A schedule controls the delay +between attempts; it does not stop an attempt that is currently running. + +##### Schedule shape + +`Schedule.exponential("10 millis")` produces 10 milliseconds, 20 milliseconds, +40 milliseconds, 80 milliseconds, and so on. `Schedule.spaced("50 millis")` +always contributes 50 milliseconds. `Schedule.either` chooses the smaller delay, +so the effective sequence is 10 milliseconds, 20 milliseconds, 40 milliseconds, +50 milliseconds, 50 milliseconds, and so on. + +`Schedule.both(Schedule.recurs(5))` makes the policy finite. `both` continues +only while both schedules continue, so the retry count stops the otherwise +unbounded capped backoff. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{ + readonly service: string + readonly status: number +}> {} + +interface AccountSummary { + readonly id: string + readonly balance: number +} + +let attempts = 0 + +const loadAccountSummary = (id: string): Effect.Effect => + Effect.gen(function*() { + attempts += 1 + yield* Console.log(`load ${id}: attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail( + new ServiceUnavailable({ + service: "accounts", + status: 503 + }) + ) + } + + return { id, balance: 125 } + }) + +const cappedBackoff = Schedule.exponential("10 millis").pipe( + Schedule.either(Schedule.spaced("50 millis")), + Schedule.both(Schedule.recurs(5)) +) + +const program = loadAccountSummary("account-123").pipe( + Effect.retry(cappedBackoff), + Effect.tap((account) => Console.log(`balance: ${account.balance}`)) +) + +Effect.runPromise(program).then(() => undefined, console.error) +// Output: +// load account-123: attempt 1 +// load account-123: attempt 2 +// load account-123: attempt 3 +// load account-123: attempt 4 +// balance: 125 +``` + +The first attempt runs immediately. If it fails, retries use the capped delay +sequence and stop after at most five retries. If every permitted attempt fails, +`Effect.retry` returns the last `ServiceUnavailable`. + +##### Variants + +For user-facing flows, use a low cap and a small retry count so the UI can move +to an error state quickly. + +For background workflows, a higher cap can be acceptable, but keep the retry +budget explicit unless another layer owns the stopping condition. + +When only some typed failures are retryable, use `Effect.retry({ schedule, +while })` and keep the predicate close to the boundary where the error is +known. + +##### Notes and caveats + +`Schedule.either` gives union-style continuation semantics and uses the minimum +delay. The minimum-delay rule is what creates the cap. + +`Schedule.both` applies the finite retry budget. Pairing the capped schedule +with `Schedule.recurs(n)` preserves the capped delay while adding the stopping +condition. + +A cap prevents excessive individual waits. It does not add jitter, read +provider-specific retry headers, or make non-idempotent work safe to retry. + +#### 5.8 Backoff with both cap and retry limit + +Most production retry policies need two bounds: the largest delay between +attempts and the maximum number of retries. + +##### Problem + +Exponential backoff alone controls pacing, not total retry effort. A cap keeps +one delay from growing too large. A retry limit stops the operation when the +dependency remains unavailable. + +Compose the two bounds explicitly: `Schedule.either` caps the delay, and +`Schedule.both(Schedule.recurs(n))` adds the finite retry budget. + +##### When to use it + +Use this for idempotent calls to HTTP APIs, queues, caches, databases, and +service clients where unlimited retrying would hold resources too long. + +This policy is easy to review because the important operational choices are +visible: base delay, maximum delay, and maximum retry count. + +##### When not to use it + +Do not use this for non-idempotent writes unless repeated execution is safe. + +Do not treat the cap as a total timeout. A policy capped at one second and +limited to five retries can still spend several seconds retrying. + +Do not rely on this alone when many clients may retry together. Add jitter or +another load-shaping mechanism for large caller populations. + +##### Schedule shape + +`Schedule.exponential("10 millis")` grows by the default factor of `2`. +Combining it with `Schedule.spaced("40 millis")` through `Schedule.either` +gives a maximum delay of 40 milliseconds. Combining that capped schedule with +`Schedule.recurs(5)` through `Schedule.both` stops after at most five retries. + +The original effect still runs immediately. The schedule is consulted only +after a typed failure. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class GatewayError extends Data.TaggedError("GatewayError")<{ + readonly status: number +}> {} + +let attempts = 0 + +const submitRequest: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`gateway attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail(new GatewayError({ status: 503 })) + } + + return "accepted" +}) + +const cappedBackoffWithLimit = Schedule.exponential("10 millis").pipe( + Schedule.either(Schedule.spaced("40 millis")), + Schedule.both(Schedule.recurs(5)) +) + +const program = submitRequest.pipe( + Effect.retry({ + schedule: cappedBackoffWithLimit, + while: (error) => error.status === 429 || error.status >= 500 + }), + Effect.tap((value) => Console.log(`result: ${value}`)) +) + +Effect.runPromise(program).then(() => undefined, console.error) +// Output: +// gateway attempt 1 +// gateway attempt 2 +// gateway attempt 3 +// gateway attempt 4 +// result: accepted +``` + +The retryable failures wait 10 milliseconds, 20 milliseconds, then at most 40 +milliseconds. If the original attempt and all five retries fail, the program +fails with the last `GatewayError`. + +##### Variants + +For an interactive request, use a smaller cap and fewer retries. For background +work, use a larger cap and budget only when the owning worker or supervisor can +afford the total time. + +When only some typed failures should be retried, keep the same schedule and +change the `while` predicate in `Effect.retry`. + +##### Notes and caveats + +Use `Schedule.either(Schedule.spaced(maxDelay))` for the cap. Use +`Schedule.both(Schedule.recurs(n))` for the retry limit. + +`Schedule.recurs(n)` counts retries after the original attempt, not total +attempts. + +The schedule output is nested composition data from `either` and `both`. Plain +`Effect.retry` uses that output for retry decisions and returns the successful +value of the retried effect. + +### 6. Retry Budgets and Deadlines + +#### 6.1 Retry for at most 10 seconds + +Use a short elapsed retry window when the caller can tolerate brief recovery +work, but not an open-ended retry loop. The schedule controls retry timing and +stopping. Error classification still belongs in the surrounding `Effect.retry` +options. + +##### Problem + +Retry transient typed failures with exponential backoff while a 10 second +schedule window remains open. The first attempt runs immediately; the window is +consulted only after a typed failure. + +##### When to use it + +Use this for idempotent service calls, gateway requests, and short dependency +recovery windows where elapsed retry time matters more than an exact retry +count. It is a good fit for temporary unavailability, overload, and network +failures that often clear quickly. + +##### When not to use it + +Do not use this as a hard timeout. `Schedule.during("10 seconds")` does not +interrupt the original attempt or any later attempt already in progress. + +Do not retry unsafe writes unless the operation has an idempotency key, +transaction boundary, de-duplication, or another guarantee that repeated +execution is safe. + +Do not use `Schedule.during` by itself for real retry traffic. It supplies a +time window, not a useful delay. + +##### Schedule shape + +`Schedule.exponential("100 millis")` produces the retry delays. With the +default factor of `2`, the delays are 100 milliseconds, 200 milliseconds, 400 +milliseconds, and so on. + +`Schedule.during("10 seconds")` supplies the elapsed schedule window. In a +retry policy, that window starts when the schedule is first stepped after the +first typed failure. + +`Schedule.both` requires both schedules to continue and uses the maximum delay. +Here the exponential schedule supplies the wait, and `during` supplies the +stopping condition. A retry decision made just inside the window may still +sleep and run the next attempt after the nominal 10 second boundary. + +##### Example + +```ts +import { Console, Data, Effect, Schedule } from "effect" + +class GatewayError extends Data.TaggedError("GatewayError")<{ + readonly reason: "Unavailable" | "Overloaded" | "BadRequest" +}> {} + +interface GatewayResponse { + readonly body: string +} + +let attempts = 0 + +const callGateway: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`gateway attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail( + new GatewayError({ + reason: attempts === 1 ? "Unavailable" : "Overloaded" + }) + ) + } + + return { body: "ok" } +}) + +const isRetryableGatewayError = (error: GatewayError): boolean => + error.reason === "Unavailable" || error.reason === "Overloaded" + +const retryForAtMost10Seconds = Schedule.exponential("100 millis").pipe( + Schedule.both(Schedule.during("10 seconds")) +) + +const program = Effect.gen(function*() { + const response = yield* callGateway.pipe( + Effect.retry({ + schedule: retryForAtMost10Seconds, + while: isRetryableGatewayError + }) + ) + + yield* Console.log(`gateway response: ${response.body}`) +}) + +Effect.runPromise(program) +``` + +`BadRequest` would stop immediately because the `while` predicate returns +`false`. If retryable failures continue until the schedule window closes, +`Effect.retry` propagates the last `GatewayError`. + +##### Variants and caveats + +Use `Schedule.spaced("500 millis").pipe(Schedule.both(Schedule.during("10 seconds")))` +when a fixed cadence is easier on the dependency than exponential backoff. + +Add `Schedule.recurs(8)` with another `Schedule.both` when eight retries is +also a real operational cap. The policy then stops when either the time budget +or the retry count is exhausted. + +Plain `Effect.retry` uses the schedule for timing and stopping. The successful +value is still the value produced by the retried effect; the composed schedule +output is not returned. + +#### 6.2 Retry for at most 1 minute + +Use a one-minute retry window when a dependency deserves a bounded recovery +period, but the caller must eventually get a result or the last typed failure. + +##### Problem + +Run the operation once immediately, then retry typed failures on a one-second +cadence while the one-minute retry window remains open. + +##### When to use it + +Use this for idempotent reads, service discovery, startup probes, short +reconnect loops, and other boundary calls where a temporary outage may clear +within a minute. + +A time window is often clearer than a retry count. Slow failed attempts produce +fewer retries inside the same minute; fast failures may produce more, but both +cases stay bounded by elapsed retry time. + +##### When not to use it + +Do not use this as an attempt timeout. `Schedule.during("1 minute")` is checked +at retry decision points and does not cancel in-flight work. + +Do not use a fixed one-second cadence for many clients that can fail together +unless synchronized retries are acceptable. Add jitter or backoff when a fleet +may retry against the same dependency. + +##### Schedule shape + +`Schedule.spaced("1 second")` supplies the retry delay. It is unbounded by +itself. + +`Schedule.during("1 minute")` supplies the elapsed retry window. It does not +add spacing. + +`Schedule.both` keeps retrying only while both sides continue and uses the +maximum delay, so the composed policy preserves the one-second cadence and +stops when the one-minute window closes. + +##### Example + +```ts +import { Console, Data, Effect, Schedule } from "effect" + +class RegistryUnavailable extends Data.TaggedError("RegistryUnavailable")<{ + readonly service: string +}> {} + +interface Endpoint { + readonly host: string + readonly port: number +} + +let attempts = 0 + +const discoverEndpoint: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`discovery attempt ${attempts}`) + + if (attempts === 1) { + return yield* Effect.fail(new RegistryUnavailable({ service: "registry" })) + } + + return { host: "api.internal", port: 443 } +}) + +const retryForAtMost1Minute = Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.during("1 minute")) +) + +const program = Effect.gen(function*() { + const endpoint = yield* discoverEndpoint.pipe( + Effect.retry(retryForAtMost1Minute) + ) + + yield* Console.log(`endpoint: ${endpoint.host}:${endpoint.port}`) +}) + +Effect.runPromise(program) +``` + +If every attempt keeps failing until the one-minute retry window closes, +`Effect.retry` propagates the last `RegistryUnavailable`. + +##### Variants and caveats + +Use `Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.during("1 minute")))` +when repeated failures should slow down over the same one-minute budget. + +Add a `while` predicate when only some typed failures should consume the retry +window. The predicate decides eligibility; the schedule still controls cadence +and duration. + +The first attempt is not delayed. The elapsed budget starts when the schedule +is first stepped after a typed failure, and a retry scheduled near the end of +the window may begin after the nominal minute. + +#### 6.3 Retry until a startup deadline + +Use this for startup gates that should wait briefly for a required dependency +before the process begins serving traffic. + +##### Problem + +Retry a readiness check with exponential backoff while a startup retry window +remains open. If readiness succeeds, startup continues. If the window closes, +startup fails with the last typed readiness error. + +##### When to use it + +Use this for databases, caches, queues, service endpoints, or local companion +processes that often become reachable shortly after the application starts. + +The readiness effect should be safe to repeat: a ping, connection probe, or +idempotent "are you ready?" call. + +##### When not to use it + +Do not use this as an ongoing health check or supervisor loop. This recipe is a +startup gate. + +Do not retry failures that prove startup is misconfigured, such as bad +credentials, invalid hosts, missing schemas, incompatible versions, or +authorization failures. + +Do not treat `Schedule.during` as a hard process deadline. It does not +interrupt a readiness attempt that is already running. + +##### Schedule shape + +`Schedule.exponential("200 millis")` supplies delays of 200 milliseconds, 400 +milliseconds, 800 milliseconds, and so on. + +`Schedule.during("30 seconds")` supplies the startup retry window. In a retry +policy, the window is checked after typed failures when the schedule decides +whether another retry is allowed. + +`Schedule.both` gives intersection semantics: both schedules must continue, +and the retry delay is the maximum of their delays. Here that means backoff +controls waiting and `during` controls when retry scheduling stops. + +##### Example + +```ts +import { Console, Data, Effect, Schedule } from "effect" + +class DependencyNotReady extends Data.TaggedError("DependencyNotReady")<{ + readonly dependency: string + readonly detail: string +}> {} + +class DependencyMisconfigured extends Data.TaggedError("DependencyMisconfigured")<{ + readonly dependency: string + readonly detail: string +}> {} + +type StartupDependencyError = DependencyNotReady | DependencyMisconfigured + +let checks = 0 + +const waitForDatabase: Effect.Effect = Effect.gen(function*() { + checks += 1 + yield* Console.log(`database readiness check ${checks}`) + + if (checks < 3) { + return yield* Effect.fail( + new DependencyNotReady({ + dependency: "database", + detail: "connection refused" + }) + ) + } +}) + +const startApplication = Console.log("application started") + +const startupReadinessPolicy = Schedule.exponential("200 millis").pipe( + Schedule.both(Schedule.during("30 seconds")) +) + +const program = Effect.gen(function*() { + yield* waitForDatabase.pipe( + Effect.retry({ + schedule: startupReadinessPolicy, + while: (error) => error._tag === "DependencyNotReady" + }) + ) + + yield* startApplication +}) + +Effect.runPromise(program) +``` + +`DependencyMisconfigured` would stop retrying immediately. It is a permanent +startup failure, not a readiness delay. + +##### Variants and caveats + +Use a gentler policy such as `Schedule.exponential("500 millis", 1.5).pipe(Schedule.both(Schedule.during("2 minutes")))` +when a dependency commonly needs longer to become ready. + +Add `Schedule.recurs(12)` with another `Schedule.both` when startup should also +have an attempt cap. + +If an individual readiness call can hang, put a timeout around that call. The +schedule bounds retry decisions; it does not cancel work already in progress. + +#### 6.4 Retry within a fixed operational budget + +Use this when retries must fit inside a known operational window while still +using a normal delay policy. + +##### Problem + +Retry with exponential backoff, but schedule more attempts only while a 30 +second elapsed retry budget remains open. + +##### When to use it + +Use this for background jobs, webhook delivery, connection setup, cache +refresh, and service calls that should get a short recovery window without +continuing indefinitely. + +This shape is useful when the caller cares more about total retry time than the +exact number of retries. Fast failures may get more attempts than slow +failures, but both are bounded by the same schedule window. + +##### When not to use it + +Do not use this as a hard deadline for an individual attempt. A schedule is +consulted after an attempt fails with a typed error; it does not interrupt +in-flight work. + +Do not use a time budget to hide permanent failures. Invalid credentials, bad +input, forbidden tenants, and misconfiguration should usually stop through a +retry predicate. + +##### Schedule shape + +`Schedule.exponential("200 millis")` supplies the retry delay. With the default +factor of `2`, it grows as 200 milliseconds, 400 milliseconds, 800 +milliseconds, 1.6 seconds, and so on. + +`Schedule.during("30 seconds")` supplies the elapsed recurrence window and no +practical delay. + +`Schedule.both` requires both sides to continue and uses the maximum delay, so +the backoff is preserved while the `during` side determines when retry +scheduling must stop. + +##### Example + +```ts +import { Console, Data, Effect, Schedule } from "effect" + +class TemporaryGatewayError extends Data.TaggedError("TemporaryGatewayError")<{ + readonly status: 429 | 500 | 502 | 503 | 504 +}> {} + +let attempts = 0 + +const callGateway: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`gateway call ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail(new TemporaryGatewayError({ status: 503 })) + } + + return "accepted" +}) + +const retryWithinBudget = Schedule.exponential("200 millis").pipe( + Schedule.both(Schedule.during("30 seconds")) +) + +const program = Effect.gen(function*() { + const result = yield* callGateway.pipe( + Effect.retry({ + schedule: retryWithinBudget, + while: (error) => error.status === 429 || error.status >= 500 + }) + ) + + yield* Console.log(`gateway result: ${result}`) +}) + +Effect.runPromise(program) +``` + +If retryable failures continue until the 30 second window closes, +`Effect.retry` returns the last `TemporaryGatewayError`. + +##### Variants and caveats + +Use `Schedule.spaced("1 second").pipe(Schedule.both(Schedule.during("20 seconds")))` +when the dependency should see a steady retry cadence inside the budget. + +Use `Schedule.exponential("50 millis").pipe(Schedule.both(Schedule.during("2 seconds")))` +for interactive paths that should retry only briefly. + +Add a count guard with `Schedule.recurs` only when the retry count is itself an +operational requirement. The number of retries inside a time budget depends on +both the delay policy and the time spent in failed attempts. + +#### 6.5 Prefer time-budget limits over attempt counts + +Use a time budget when the requirement is about latency, not about the number +of times an operation may run. + +##### What this section is about + +An attempt count answers "how many retries may be scheduled after the original +attempt?" A time budget answers "how long may this retry window stay open?" +Those are related, but not interchangeable. + +In Effect, the usual shape is a delay schedule combined with +`Schedule.during`. The delay schedule controls cadence. `Schedule.during` +controls the elapsed retry window. + +##### Why it matters + +Fixed retry counts are easy to read but weak as latency limits. Three retries +can finish almost immediately when failures are fast, or take much longer when +each failed attempt waits on a network boundary before returning a typed +failure. + +Time budgets express the boundary most callers care about: how long they are +willing to keep retrying. A startup check may get two minutes. A background job +may get 30 seconds. A user-facing request may get only a brief recovery window. + +##### Core idea + +Start with the delay shape, then add the budget with `Schedule.both`. Because +`both` requires both schedules to continue, the policy stops when the elapsed +window closes. Because `both` uses the maximum delay, the retry cadence still +comes from `Schedule.spaced`, `Schedule.fixed`, or `Schedule.exponential`. + +Use `Schedule.recurs` as a secondary guard only when the count itself is a real +requirement. + +##### Example + +```ts +import { Console, Data, Effect, Schedule } from "effect" + +class RemoteBusy extends Data.TaggedError("RemoteBusy")<{ + readonly attempt: number +}> {} + +let attempts = 0 + +const callRemote: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`remote attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail(new RemoteBusy({ attempt: attempts })) + } + + return "remote value" +}) + +const retryWithinLatencyBudget = Schedule.exponential("50 millis").pipe( + Schedule.both(Schedule.during("1 second")) +) + +const program = Effect.gen(function*() { + const value = yield* callRemote.pipe( + Effect.retry(retryWithinLatencyBudget) + ) + + yield* Console.log(`completed with: ${value}`) +}) + +Effect.runPromise(program) +``` + +This policy does not promise exactly three retries. It retries according to the +backoff schedule while the one-second retry window is open. + +##### Common mistakes + +Do not treat `Schedule.recurs(3)` as a latency budget. It limits retry count, +not elapsed time. + +Do not use `Schedule.during` by itself for production retry policies. It has no +useful spacing on its own, so a fast-failing effect can retry aggressively +until the window closes. + +Do not choose a time budget to hide the wrong retry predicate. Permanent +failures such as bad input, invalid credentials, forbidden access, and +misconfiguration should usually stop immediately. + +##### Practical guidance + +Use `Schedule.exponential` when repeated failures should slow down over time. +Use `Schedule.spaced` when the cadence should be steady. Use `Schedule.fixed` +when retries should align to a fixed-rate interval instead of waiting a fixed +duration after each failed attempt completes. + +Add `Schedule.recurs` only as a secondary cap when the number of retries is +operationally meaningful. For most service-boundary code, the time budget is +the clearer primary limit because it matches the caller's waiting tolerance. + +### 7. Error-Aware Retries + +#### 7.1 Retry only transient failures + +Use a retry predicate when only some typed failures should spend retry budget. +The schedule controls timing and limits; the predicate controls eligibility. + +##### Problem + +An operation can fail for temporary reasons and permanent reasons. Retry the +temporary cases, such as timeouts, rate limits, and service unavailability. +Return invalid input, authorization failures, and unsupported operations +immediately. + +##### When to use it + +Use this when the effect has a meaningful typed error channel and only part of +that channel is retryable. It fits HTTP clients, database calls, cache fills, +message publishing, and dependency probes. + +The operation must still be safe to run again for the selected failures. Reads +are usually safe. Writes need an idempotency key, transaction boundary, or +another duplicate-safety guarantee. + +##### When not to use it + +Do not classify every operational failure as transient. Authentication, +authorization, validation, decoding, and unsupported-operation errors usually +need a different handler, not another attempt. + +Do not retry a large workflow when only one boundary call is transient. Put +`Effect.retry` around the smallest effect that can safely run more than once. + +##### Schedule shape + +With `Effect.retry`, the schedule input is the typed failure from the effect. +The options form accepts `while` and `until` predicates over that same error +type. + +`while` means "continue while this predicate is true." `until` means "continue +until this predicate becomes true." If a predicate and a finite schedule are +both present, both must allow another attempt. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class Timeout extends Data.TaggedError("Timeout")<{ + readonly operation: string +}> {} + +class RateLimited extends Data.TaggedError("RateLimited")<{ + readonly retryAfterMillis: number +}> {} + +class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{ + readonly status: 503 | 504 +}> {} + +class InvalidRequest extends Data.TaggedError("InvalidRequest")<{ + readonly message: string +}> {} + +class Unauthorized extends Data.TaggedError("Unauthorized")<{ + readonly reason: "MissingToken" | "ExpiredToken" +}> {} + +type ApiError = + | Timeout + | RateLimited + | ServiceUnavailable + | InvalidRequest + | Unauthorized + +interface ApiResponse { + readonly id: string + readonly status: "accepted" +} + +const isTransientApiError = (error: ApiError): boolean => + error._tag === "Timeout" || + error._tag === "RateLimited" || + error._tag === "ServiceUnavailable" + +const retryTransientFailures = Schedule.spaced("50 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +const makeRequest = ( + label: string, + failures: ReadonlyArray +): Effect.Effect => { + let attempt = 0 + + return Effect.gen(function*() { + attempt += 1 + yield* Console.log(`${label}: attempt ${attempt}`) + + const failure = failures[attempt - 1] + if (failure !== undefined) { + return yield* Effect.fail(failure) + } + + return { id: label, status: "accepted" } + }) +} + +const runRequest = ( + label: string, + request: Effect.Effect +) => + request.pipe( + Effect.retry({ + schedule: retryTransientFailures, + while: isTransientApiError + }), + Effect.matchEffect({ + onFailure: (error) => Console.log(`${label}: failed with ${error._tag}`), + onSuccess: (response) => Console.log(`${label}: ${response.status}`) + }) + ) + +const program = Effect.gen(function*() { + yield* runRequest( + "transient", + makeRequest("transient", [ + new Timeout({ operation: "create-job" }), + new ServiceUnavailable({ status: 503 }) + ]) + ) + + yield* runRequest( + "permanent", + makeRequest("permanent", [ + new InvalidRequest({ message: "missing id" }) + ]) + ) +}) + +Effect.runPromise(program) +// Output: +// transient: attempt 1 +// transient: attempt 2 +// transient: attempt 3 +// transient: accepted +// permanent: attempt 1 +// permanent: failed with InvalidRequest +``` + +The transient request retries and then succeeds. The permanent request stops +after the first `InvalidRequest`. + +##### Variants and caveats + +Use `until` when the stopping condition is clearer, for example "retry until a +permanent error is observed." + +Without `schedule` or `times`, retry options built only from `while` or +`until` can retry indefinitely while the predicate allows it. + +The predicate sees typed failures from the error channel. Defects and fiber +interruptions are not retried as typed failures. + +#### 7.2 Do not retry validation errors + +Retry can handle temporary failures, but it should not hide bad input. Keep +validation failures explicit in the typed error channel and exclude them from +the retry predicate. + +##### Problem + +One operation may fail with transient service errors or permanent request +errors. Retry the transient cases. Return validation and conflict errors +immediately. + +##### When to use it + +Use this for form submission, API clients, command handlers, queue consumers, +and service calls that validate payloads before or during a boundary request. + +Use structured typed errors so the retry decision comes from tags and fields, +not from parsing error messages. + +##### When not to use it + +Do not retry invalid input in the hope that it becomes valid. Missing fields, +unsupported enum values, malformed payloads, and domain-rule failures require a +different request. + +Do not wrap a large workflow in retry just to handle one transient call. Retry +the smallest idempotent operation that may safely run again. + +##### Schedule shape + +The schedule should still be finite. A common shape is exponential backoff plus +`Schedule.recurs`. + +After each typed failure, `while` is checked first. If it returns `false`, +retrying stops with that failure. If it returns `true`, the schedule decides +whether another retry is available and how long to wait. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +interface RegistrationInput { + readonly email: string + readonly plan: "Free" | "Pro" +} + +interface Registration { + readonly id: string + readonly email: string +} + +class ValidationError extends Data.TaggedError("ValidationError")<{ + readonly field: string + readonly message: string +}> {} + +class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{ + readonly service: "Accounts" | "Billing" +}> {} + +class RateLimited extends Data.TaggedError("RateLimited")<{ + readonly retryAfterMillis: number +}> {} + +class ConflictError extends Data.TaggedError("ConflictError")<{ + readonly resource: "Email" +}> {} + +type RegistrationError = + | ValidationError + | ServiceUnavailable + | RateLimited + | ConflictError + +const isRetryableRegistrationError = (error: RegistrationError): boolean => + error._tag === "ServiceUnavailable" || error._tag === "RateLimited" + +const retryTransientFailures = Schedule.exponential("50 millis").pipe( + Schedule.both(Schedule.recurs(4)) +) + +const submitRegistration = ( + input: RegistrationInput +): Effect.Effect => { + let attempt = 0 + + return Effect.gen(function*() { + attempt += 1 + yield* Console.log(`${input.email}: submit attempt ${attempt}`) + + if (!input.email.includes("@")) { + return yield* Effect.fail( + new ValidationError({ + field: "email", + message: "must contain @" + }) + ) + } + + if (attempt === 1) { + return yield* Effect.fail(new ServiceUnavailable({ service: "Accounts" })) + } + + return { id: `registration-${attempt}`, email: input.email } + }) +} + +const runRegistration = (input: RegistrationInput) => + submitRegistration(input).pipe( + Effect.retry({ + schedule: retryTransientFailures, + while: isRetryableRegistrationError + }), + Effect.matchEffect({ + onFailure: (error) => Console.log(`${input.email}: failed with ${error._tag}`), + onSuccess: (registration) => Console.log(`${input.email}: ${registration.id}`) + }) + ) + +const program = Effect.gen(function*() { + yield* runRegistration({ email: "ada@example.com", plan: "Pro" }) + yield* runRegistration({ email: "not-an-email", plan: "Free" }) +}) + +Effect.runPromise(program) +// Output: +// ada@example.com: submit attempt 1 +// ada@example.com: submit attempt 2 +// ada@example.com: registration-2 +// not-an-email: submit attempt 1 +// not-an-email: failed with ValidationError +``` + +The valid registration retries a temporary account-service failure. The invalid +email fails once with `ValidationError` and does not spend retry budget. + +##### Variants and caveats + +Use `until` when the non-retryable cases are the smaller or clearer set. + +Use an effectful predicate only when retryability genuinely depends on an +external policy, such as a feature flag or runtime service. If that predicate +fails, its failure is propagated instead of retrying. + +`while` and `until` inspect typed failures after an attempt fails. They do not +inspect successful values, and they do not prevent the original attempt from +running. + +#### 7.3 Retry only on timeouts + +Use this when timeout is the only retryable typed failure for an operation. + +##### Problem + +Build a finite retry policy that accepts only the typed timeout failure. Other +failures, such as HTTP status errors or decode errors, should fail fast. + +##### When to use it + +Use this when the error channel distinguishes timeouts from other failures. The +timeout must be part of the typed error model, not a string embedded in a +generic exception. + +This fits HTTP clients, database calls, RPC clients, queues, reconnect probes, +and idempotent writes protected by duplicate-safety guarantees. + +##### When not to use it + +Do not retry every typed error just because timeout is one possible case. +Passing only `Schedule.recurs(3)` or `Schedule.exponential("100 millis")` to +`Effect.retry` retries every typed failure from the effect. + +Do not assume timeout means the remote side did nothing. A write timeout can +mean the response was lost after the remote side completed the operation. + +##### Schedule shape + +The schedule supplies the finite retry policy. The `while` predicate prevents +non-timeout failures from consuming that policy. + +Read the policy as: run once immediately; after a typed timeout, back off and +retry while attempts remain; after any non-timeout typed failure, stop +immediately. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +interface Invoice { + readonly id: string + readonly total: number +} + +class RequestTimeout extends Data.TaggedError("RequestTimeout")<{ + readonly operation: "lookup-invoice" +}> {} + +class HttpFailure extends Data.TaggedError("HttpFailure")<{ + readonly status: number +}> {} + +class DecodeFailure extends Data.TaggedError("DecodeFailure")<{ + readonly message: string +}> {} + +type LookupInvoiceError = RequestTimeout | HttpFailure | DecodeFailure + +const isRequestTimeout = ( + error: LookupInvoiceError +): error is RequestTimeout => error._tag === "RequestTimeout" + +const timeoutRetryPolicy = Schedule.exponential("50 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +const makeLookupInvoice = ( + label: string, + failures: ReadonlyArray +): Effect.Effect => { + let attempt = 0 + + return Effect.gen(function*() { + attempt += 1 + yield* Console.log(`${label}: lookup attempt ${attempt}`) + + const failure = failures[attempt - 1] + if (failure !== undefined) { + return yield* Effect.fail(failure) + } + + return { id: "inv-123", total: 42 } + }) +} + +const runLookup = ( + label: string, + lookup: Effect.Effect +) => + lookup.pipe( + Effect.retry({ + schedule: timeoutRetryPolicy, + while: isRequestTimeout + }), + Effect.matchEffect({ + onFailure: (error) => Console.log(`${label}: failed with ${error._tag}`), + onSuccess: (invoice) => Console.log(`${label}: invoice ${invoice.id}`) + }) + ) + +const program = Effect.gen(function*() { + yield* runLookup( + "timeout-recovers", + makeLookupInvoice("timeout-recovers", [ + new RequestTimeout({ operation: "lookup-invoice" }), + new RequestTimeout({ operation: "lookup-invoice" }) + ]) + ) + + yield* runLookup( + "http-failure", + makeLookupInvoice("http-failure", [ + new HttpFailure({ status: 403 }) + ]) + ) +}) + +Effect.runPromise(program) +// Output: +// timeout-recovers: lookup attempt 1 +// timeout-recovers: lookup attempt 2 +// timeout-recovers: lookup attempt 3 +// timeout-recovers: invoice inv-123 +// http-failure: lookup attempt 1 +// http-failure: failed with HttpFailure +``` + +The timeout case retries and succeeds. The HTTP failure stops after the first +attempt because the predicate returns `false`. + +##### Variants and caveats + +If the timeout comes from `Effect.timeout`, the typed timeout failure is +`Cause.TimeoutError`; use `Cause.isTimeoutError` as the predicate when the +effect can also fail with domain errors. + +Keep timeout retry bounded with `Schedule.recurs`, `times`, +`Schedule.during`, or another stopping condition unless retrying forever is +intentional. + +The first attempt is not delayed. Schedule delays apply only after a typed +failure has been accepted by the retry policy. + +#### 7.4 Retry only on 5xx responses + +Use this when an HTTP adapter should retry temporary server responses but +return client-side failures immediately. + +##### Problem + +Keep the HTTP status in the typed error and retry only responses from 500 +through 599. Server failures such as 500, 502, 503, and 504 may be temporary. +Most 4xx statuses need a different request, resource, or credential. + +##### When to use it + +Use this when the effect's error channel contains a structured HTTP response +error. It fits service clients, API adapters, webhooks, and gateway calls where +retryability follows the response class. + +It is safest for idempotent reads and duplicate-safe writes. A 5xx response +does not prove the server skipped the side effect. + +##### When not to use it + +Do not retry all HTTP failures. Most 4xx statuses represent request, +authorization, missing-resource, or conflict failures. + +Do not treat this as a rate-limit policy. `429 Too Many Requests` is not a 5xx +response and usually needs timing from `Retry-After`, caller budgets, or +admission control. + +##### Schedule shape + +`Effect.retry` feeds each typed HTTP failure to the `while` predicate. If the +predicate returns `false`, retrying stops with that failure. If it returns +`true`, the finite schedule decides whether another retry is available and how +long to wait. + +For most clients, combine the predicate with a finite backoff schedule. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +type HttpStatus = + | 400 + | 401 + | 403 + | 404 + | 409 + | 422 + | 500 + | 501 + | 502 + | 503 + | 504 + +class HttpResponseError extends Data.TaggedError("HttpResponseError")<{ + readonly method: "GET" | "POST" + readonly url: string + readonly status: HttpStatus +}> {} + +interface User { + readonly id: string + readonly name: string +} + +const is5xxResponse = (error: HttpResponseError): boolean => error.status >= 500 && error.status < 600 + +const retryWithBackoff = Schedule.exponential("50 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +const makeRequestUser = ( + label: string, + failures: ReadonlyArray +): Effect.Effect => { + let attempt = 0 + + return Effect.gen(function*() { + attempt += 1 + yield* Console.log(`${label}: HTTP attempt ${attempt}`) + + const failure = failures[attempt - 1] + if (failure !== undefined) { + return yield* Effect.fail(failure) + } + + return { id: "user-123", name: "Ada" } + }) +} + +const runRequest = ( + label: string, + request: Effect.Effect +) => + request.pipe( + Effect.retry({ + schedule: retryWithBackoff, + while: is5xxResponse + }), + Effect.matchEffect({ + onFailure: (error) => Console.log(`${label}: failed with HTTP ${error.status}`), + onSuccess: (user) => Console.log(`${label}: user ${user.name}`) + }) + ) + +const program = Effect.gen(function*() { + yield* runRequest( + "server-recovers", + makeRequestUser("server-recovers", [ + new HttpResponseError({ method: "GET", url: "/users/123", status: 503 }), + new HttpResponseError({ method: "GET", url: "/users/123", status: 502 }) + ]) + ) + + yield* runRequest( + "client-error", + makeRequestUser("client-error", [ + new HttpResponseError({ method: "GET", url: "/users/missing", status: 404 }) + ]) + ) +}) + +Effect.runPromise(program) +// Output: +// server-recovers: HTTP attempt 1 +// server-recovers: HTTP attempt 2 +// server-recovers: HTTP attempt 3 +// server-recovers: user Ada +// client-error: HTTP attempt 1 +// client-error: failed with HTTP 404 +``` + +The server-error case retries and succeeds. The 404 case stops immediately. + +##### Variants and caveats + +Use an allow-list when some 5xx responses are permanent for your API, for +example retrying 500, 502, 503, and 504 but not 501. + +Use `status >= 500 && status < 600` unless your adapter intentionally treats +nonstandard status codes as server failures. + +Keep rate limiting as a sibling policy. A `429` may be retryable, but it +usually needs different timing and admission-control behavior from generic 5xx +responses. + +#### 7.5 Treat rate limits differently from server errors + +Rate limits and server failures can both be transient, but they communicate +different operational signals. Preserve that difference in the typed error +model and choose a schedule for each case. + +##### Problem + +`503 Service Unavailable` usually means the server failed to handle the +request. `429 Too Many Requests` means the caller is applying too much +pressure. A single generic retry policy hides that distinction. + +##### Why this comparison matters + +For retryable 5xx responses, a short jittered backoff is often enough: probe +again, spread callers around each delay, and stop after a small budget. + +For rate limits, prefer provider guidance. If the response carries a +`Retry-After` value or equivalent metadata, use that value instead of guessing +from a generic exponential sequence. + +##### Schedule shape + +Use a finite jittered backoff for server errors. Use a rate-limit-specific +schedule that can read the typed retry input when the error carries the wait +duration. + +`Schedule.identity()` outputs the retry input as the schedule output. +`Schedule.addDelay` can then derive the wait from that output. + +##### Example + +```ts runnable deterministic +import { Console, Data, Duration, Effect, Schedule } from "effect" + +class ServerError extends Data.TaggedError("ServerError")<{ + readonly status: 500 | 502 | 503 | 504 +}> {} + +class RateLimited extends Data.TaggedError("RateLimited")<{ + readonly retryAfterMillis: number +}> {} + +let serverAttempts = 0 + +const callServer: Effect.Effect = Effect.gen(function*() { + serverAttempts += 1 + yield* Console.log(`server attempt ${serverAttempts}`) + + if (serverAttempts < 3) { + return yield* Effect.fail(new ServerError({ status: 503 })) + } + + return "server value" +}) + +let rateLimitAttempts = 0 + +const callRateLimitedApi: Effect.Effect = Effect.gen(function*() { + rateLimitAttempts += 1 + yield* Console.log(`rate-limit attempt ${rateLimitAttempts}`) + + if (rateLimitAttempts === 1) { + return yield* Effect.fail(new RateLimited({ retryAfterMillis: 100 })) + } + + return "rate-limited value" +}) + +const retryServerErrors = Schedule.exponential("50 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(3)) +) + +const retryRateLimits = Schedule.identity().pipe( + Schedule.both(Schedule.recurs(2)), + Schedule.addDelay(([error]) => Effect.succeed(Duration.millis(error.retryAfterMillis))) +) + +const program = Effect.gen(function*() { + const serverValue = yield* callServer.pipe( + Effect.retry({ + schedule: retryServerErrors, + while: (error) => error.status >= 500 && error.status < 600 + }) + ) + yield* Console.log(`server result: ${serverValue}`) + + const rateLimitedValue = yield* callRateLimitedApi.pipe( + Effect.retry({ + schedule: retryRateLimits, + while: (error) => error._tag === "RateLimited" + }) + ) + yield* Console.log(`rate-limit result: ${rateLimitedValue}`) +}) + +Effect.runPromise(program) +// Output: +// server attempt 1 +// server attempt 2 +// server attempt 3 +// server result: server value +// rate-limit attempt 1 +// rate-limit attempt 2 +// rate-limit result: rate-limited value +``` + +The server policy retries quickly with jitter. The rate-limit policy waits from +the typed retry hint. + +##### Tradeoffs + +The 5xx policy works even when the server gives no retry hint, but it is only a +guess. Keep its retry budget small. + +The rate-limit policy is more protocol-aware. It works best when the adapter +preserves `Retry-After` or equivalent quota metadata in the typed error. + +##### Recommended default + +Do not put `429` into the same predicate as generic 5xx failures. Use a +dedicated `RateLimited` error, preserve the retry delay when available, and use +a small retry count. + +For retryable 5xx responses, use finite jittered exponential backoff. For rate +limits, prefer provider guidance first and fall back to a fixed or capped delay +only when no retry hint exists. + +Retried writes still need idempotency, de-duplication, or a transaction +boundary. A better retry policy does not make duplicate side effects safe. + +### 8. Idempotency and Retry Safety + +#### 8.1 Safe retries for GET requests + +GET requests are usually safe to retry because they are meant to read state, not +change it. This recipe keeps read retries bounded, typed, and explicit. + +##### Problem + +Retrying a GET rarely risks duplicate mutation. The real risks are unbounded +traffic, hidden persistent failures, and caller latency that grows past its +budget. + +Keep the retry boundary around the single read. Use a finite `Schedule` for +delay and attempt budget, and use a predicate on the typed failure to retry only +transient errors. + +##### When to use it + +Use this recipe for read-only HTTP calls: fetching a resource, checking status, +looking up metadata, refreshing a view model, or filling a cache from a remote +source. + +It also fits replaceable values. If the first attempt fails and a later attempt +succeeds, the caller observes only the final read result. + +Keep the policy bounded even for safe reads. A GET can still consume connection +slots, server CPU, database capacity, and caller latency budget. + +##### When not to use it + +Do not use this section as a complete policy for writes, even if the endpoint is +named like a read. Duplicate-safe writes, idempotency keys, and mutation retry +safety belong in sibling sections. + +Do not retry every GET failure. A malformed URL, authorization failure, missing +resource, or response decode error is unlikely to improve by waiting. + +Do not wrap a large workflow in a retry only because one step is a GET. Retry +the read itself, before local state changes, notifications, or other effects are +performed. + +##### Schedule shape + +For GET requests, the usual shape is: + +- a small initial delay, often with exponential backoff +- jitter, so many callers do not retry at the same instant +- a finite retry count, time budget, or both +- an error predicate that allows only transient failures + +`Schedule.exponential("10 millis")` provides the backoff delays in the example +below; production values are usually larger. +`Schedule.jittered` randomly adjusts each delay between 80% and 120% of the +original delay. `Schedule.both(Schedule.recurs(3))` keeps the policy finite: +both schedules must continue, so the read gets at most three retries after the +original attempt. + +With `Effect.retry`, the GET runs once immediately. Only failures from the typed +error channel are retried, and only while the predicate returns `true`. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class GetUserError extends Data.TaggedError("GetUserError")<{ + readonly reason: "Timeout" | "ConnectionReset" | "BadGateway" | "NotFound" | "Unauthorized" | "DecodeError" +}> {} + +interface User { + readonly id: string + readonly name: string +} + +let attempts = 0 + +const getUser = (id: string): Effect.Effect => + Effect.gen(function*() { + attempts += 1 + yield* Console.log(`GET /users/${id} attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail(new GetUserError({ reason: "Timeout" })) + } + + return { id, name: "Ada" } + }) + +const isRetryableGetFailure = (error: GetUserError): boolean => { + switch (error.reason) { + case "Timeout": + case "ConnectionReset": + case "BadGateway": + return true + case "NotFound": + case "Unauthorized": + case "DecodeError": + return false + } +} + +const safeGetRetryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(3)) +) + +const program = getUser("user-123").pipe( + Effect.retry({ + schedule: safeGetRetryPolicy, + while: isRetryableGetFailure + }) +).pipe( + Effect.tap((user) => Console.log(`loaded ${user.name}`)) +) + +Effect.runPromise(program) +// Output: +// GET /users/user-123 attempt 1 +// GET /users/user-123 attempt 2 +// GET /users/user-123 attempt 3 +// loaded Ada +``` + +`program` performs the GET once, then retries only timeout, connection reset, or +bad gateway failures. A `NotFound`, `Unauthorized`, or `DecodeError` failure +stops immediately and is returned. + +The same shape works for cache fills. Keep the cache write outside the retried +GET if the cache layer writes only after a successful read. That way each retry +is still just another attempt to obtain the same value. + +##### Variants + +For status lookups that are cheap and user-facing, use fewer retries and a +smaller delay. For background cache refreshes, use a slower base delay and a +slightly larger budget. The reads are still safe, but the downstream service may +already be under pressure. + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +const statusLookupRetryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(2)) +) + +const cacheRefreshRetryPolicy = Schedule.exponential("20 millis", 1.5).pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +let statusAttempts = 0 + +const readStatus = Effect.gen(function*() { + statusAttempts += 1 + yield* Console.log(`status attempt ${statusAttempts}`) + if (statusAttempts < 2) return yield* Effect.fail("transient") + return "ok" +}) + +const program = Effect.gen(function*() { + const status = yield* readStatus.pipe(Effect.retry(statusLookupRetryPolicy)) + yield* Console.log(`status: ${status}`) + yield* Console.log(`cache refresh policy ready: ${Schedule.isSchedule(cacheRefreshRetryPolicy)}`) +}) + +Effect.runPromise(program) +// Output: +// status attempt 1 +// status attempt 2 +// status: ok +// cache refresh policy ready: true +``` + +The status policy gives the caller a quick second and third chance. The cache +policy is slower and broader, which is appropriate only when the caller can +tolerate more latency. + +For observability, attach logging or metrics around the retried GET rather than +changing the schedule into an unbounded one. Count attempts, final failures, and +latency separately so a safe retry policy remains visible in production. + +##### Notes and caveats + +Safe does not mean free. Retried GET requests can amplify traffic during an +incident, especially when many callers share the same policy. + +The first GET is not delayed. The schedule is consulted only after the effect +fails with a typed error. + +`Schedule.exponential` does not stop by itself. Pair it with `Schedule.recurs`, a +time budget, or another stopping condition. + +`Schedule.recurs(3)` means three retries after the original attempt, not three +total attempts. + +Jitter is usually appropriate for service calls and cache fills. It is less +important for a single local caller, but it becomes valuable as soon as many +fibers, processes, or hosts can fail at the same time. + +Keep retry predicates explicit. A GET that returns `404 Not Found` for a real +missing resource should normally fail fast, while a timeout or gateway failure +may be worth another attempt. + +#### 8.2 Retrying idempotent writes + +An idempotent write is a write where repeating the same request has the same +effect as running it once. This recipe places `Schedule` around that +duplicate-safe boundary. + +##### Problem + +Ambiguous write failures are dangerous because the caller may see a timeout, +dropped connection, or temporary service error after the remote system has +already applied the change. Retrying an ordinary write can create a duplicate +charge, send a second notification, insert a second row, or publish the same +command twice. + +Retry the write only when the operation is designed to be duplicate-safe. Then +use `Schedule` to make the retry policy finite, delayed, and visible. + +The schedule is not the safety mechanism. It only says when to try again. The +write contract must make repeated attempts equivalent to one logical write. + +##### When to use it + +Use this recipe when the write is explicitly idempotent or duplicate-safe. Good +examples include setting a resource to a known value, upserting by a stable +identifier, acknowledging a message with broker-level deduplication, or writing +to an endpoint that treats repeated equivalent requests as the same logical +operation. + +It also fits writes where the downstream system documents that a retry after a +transport failure is safe. In those cases, the schedule handles transient timing +problems while the protocol handles duplicate attempts. + +Keep the retry around the smallest duplicate-safe write. If a workflow contains +reads, validation, and one idempotent write, retry the write effect itself. + +##### When not to use it + +Do not use retries to make non-idempotent writes safe. If repeating the operation +can create additional business effects, add a domain-level safety mechanism +before adding a schedule. + +Do not retry ambiguous writes that depend on hidden server state unless the +server gives a documented duplicate-safe contract. A finite retry limit reduces +damage, but it does not change the semantics of the write. + +Do not retry validation failures, authorization failures, malformed payloads, or +business-rule rejections. Those errors are usually permanent and should be +returned immediately. + +##### Schedule shape + +For duplicate-safe writes, prefer a bounded policy with backoff and jitter. The +example uses short demo delays; production values are usually larger. +`Schedule.exponential` spaces retries farther apart after repeated failures. +`Schedule.jittered` spreads concurrent callers around each computed delay. +`Schedule.recurs(4)` limits the policy to at most four retries after the original +attempt. + +With `Effect.retry`, the write runs once immediately. If it fails with a typed +error, that error is fed to the schedule. The schedule decides whether another +attempt is allowed and how long to wait before that attempt. If all retries are +exhausted, `Effect.retry` returns the last typed failure. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class WriteTimeout extends Data.TaggedError("WriteTimeout")<{ + readonly operation: "SetAccountEmail" +}> {} + +class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{ + readonly status: 503 | 504 +}> {} + +class InvalidEmail extends Data.TaggedError("InvalidEmail")<{ + readonly email: string +}> {} + +type WriteError = WriteTimeout | ServiceUnavailable | InvalidEmail + +let attempts = 0 + +const setAccountEmail = ( + accountId: string, + email: string +): Effect.Effect => + Effect.gen(function*() { + attempts += 1 + yield* Console.log(`set email attempt ${attempts} for ${accountId}`) + + if (!email.includes("@")) { + return yield* Effect.fail(new InvalidEmail({ email })) + } + + if (attempts < 3) { + return yield* Effect.fail(new WriteTimeout({ operation: "SetAccountEmail" })) + } + + yield* Console.log(`stored ${email}`) + }) + +const retryDuplicateSafeWrite = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const updateEmail = (accountId: string, email: string) => + setAccountEmail(accountId, email).pipe( + Effect.retry({ + schedule: retryDuplicateSafeWrite, + while: (error) => error._tag === "WriteTimeout" || error._tag === "ServiceUnavailable" + }) + ) + +const program = updateEmail("account-1", "ada@example.com") + +Effect.runPromise(program) +// Output: +// set email attempt 1 for account-1 +// set email attempt 2 for account-1 +// set email attempt 3 for account-1 +// stored ada@example.com +``` + +This example assumes `setAccountEmail(accountId, email)` is duplicate-safe: +running it more than once sets the same account to the same email address. A +timeout or temporary service failure can be retried. An `InvalidEmail` failure is +not retried because repeating the same invalid write will not make it valid. + +##### Variants + +Use a fixed delay when the downstream system prefers steady retry traffic, or a +larger background policy when the caller can tolerate more latency: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +const steadyWriteRetry = Schedule.spaced("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(3)) +) + +const backgroundWriteRetry = Schedule.exponential("20 millis", 2).pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +let attempts = 0 + +const writeAuditMarker = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`audit marker attempt ${attempts}`) + if (attempts < 2) return yield* Effect.fail("service-unavailable") + return "stored" +}) + +const program = Effect.gen(function*() { + const result = yield* writeAuditMarker.pipe(Effect.retry(steadyWriteRetry)) + yield* Console.log(`steady policy result: ${result}`) + yield* Console.log(`background policy ready: ${Schedule.isSchedule(backgroundWriteRetry)}`) +}) + +Effect.runPromise(program) +// Output: +// audit marker attempt 1 +// audit marker attempt 2 +// steady policy result: stored +// background policy ready: true +``` + +The fixed schedule still limits retries and adds jitter, but avoids exponential +growth. The longer background schedule is a throughput and latency choice, not +an idempotency guarantee. + +##### Notes and caveats + +Idempotency keys are one common way to make a write duplicate-safe, but they are +not the focus of this recipe. The important point here is the contract: repeated +attempts of the same logical write must not create additional business effects. + +Retry only typed failures that plausibly mean the write outcome is unknown or +temporarily unavailable, such as timeouts, connection loss, rate limits, or 5xx +responses. Keep permanent failures out of the retry path with `while` or +`until`. + +The first attempt is not delayed. The schedule controls only the waits between +failed attempts. + +For user-facing writes, keep retry budgets small. If the operation may still be +running remotely after the caller gives up, expose a way to observe the final +state rather than asking the user to submit a second independent write. + +#### 8.3 Why non-idempotent retries are dangerous + +Non-idempotent work changes external state each time it runs. A retry schedule +can limit attempts, but it cannot make repeated charges, emails, shipments, or +webhooks semantically safe. + +##### The anti-pattern + +A retry policy is easy to attach to any failing effect: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let attempts = 0 +let chargesAccepted = 0 + +const chargeCustomer = Effect.gen(function*() { + attempts += 1 + chargesAccepted += 1 + yield* Console.log(`attempt ${attempts}: provider accepted charge #${chargesAccepted}`) + + if (attempts === 1) { + return yield* Effect.fail("response-lost") + } + + return "charged" +}) + +const retryWrites = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +const program = chargeCustomer.pipe( + Effect.retry(retryWrites), + Effect.tap((result) => Console.log(`${result}; accepted charges: ${chargesAccepted}`)) +) + +Effect.runPromise(program) +// Output: +// attempt 1: provider accepted charge #1 +// attempt 2: provider accepted charge #2 +// charged; accepted charges: 2 +``` + +This shape is technically valid, and the example terminates quickly, but it +models the danger: the first attempt can be accepted by the provider and still +fail from the caller's point of view. `Effect.retry` then runs the same +side-effecting operation again. + +The same anti-pattern appears with email delivery, inventory updates, shipment +creation, ticket creation, one-way webhook calls, and external systems that do +not give the caller a reliable duplicate-suppression boundary. + +##### Why it happens + +Retries are driven by what the caller can observe. A timeout, dropped +connection, `503`, or connection reset tells the caller that it did not receive +a successful response. It does not prove that the downstream system did +nothing. + +For a read, this uncertainty is usually acceptable. Running the same lookup +again normally produces another observation of the same resource. For a write, +the uncertainty crosses a boundary: the downstream service may have committed +the side effect and then failed before the response reached the caller. + +`Schedule` controls when and how often the effect is attempted again. It does +not change the meaning of the side effect being retried. A careful schedule can +reduce load and limit attempts, but it cannot make a non-idempotent operation +safe by itself. + +##### Why it is risky + +Non-idempotent retries turn ambiguous failures into duplicate business actions. +A payment retry can double-charge a customer. An email retry can send the same +message multiple times. An inventory retry can decrement stock twice. A webhook +retry can trigger another system to create duplicate records. + +The operational damage is often larger than the immediate failure. Duplicate +charges need refunds and support handling. Duplicate emails erode trust and may +trip abuse controls. Duplicate inventory updates can oversell products or block +valid orders. Duplicate one-way calls are hard to unwind because the caller may +not own the downstream state. + +Attempt limits do not remove this risk: + +```ts +import { Console, Effect, Schedule } from "effect" + +const boundedButStillUnsafe = Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.recurs(2)) +) + +const program = Console.log(`bounded policy: ${Schedule.isSchedule(boundedButStillUnsafe)}`) + +Effect.runPromise(program) +``` + +This policy limits the damage to two retries after the original attempt. It +does not answer the important question: is it safe for the external side effect +to happen three times? + +##### A better approach + +Place retries around effects that are safe to re-run, and keep unsafe writes +outside generic retry wrappers unless the external protocol provides a +duplicate-safe boundary. + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let reservationAttempts = 0 + +const reserveLocalOrderNumber = Effect.gen(function*() { + reservationAttempts += 1 + yield* Console.log(`reserve order number attempt ${reservationAttempts}`) + if (reservationAttempts < 2) return yield* Effect.fail("local-store-busy") + return "order-1001" +}) + +const submitChargeOnce = (orderNumber: string) => Console.log(`submit one charge for ${orderNumber}`) + +const retryTransientPreparation = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +const program = Effect.gen(function*() { + const orderNumber = yield* reserveLocalOrderNumber.pipe( + Effect.retry(retryTransientPreparation) + ) + + yield* submitChargeOnce(orderNumber) +}) + +Effect.runPromise(program) +// Output: +// reserve order number attempt 1 +// reserve order number attempt 2 +// submit one charge for order-1001 +``` + +Here the schedule is applied only to the preparation step that the application +has decided is safe to repeat. The external charge is a separate step. If that +charge returns an ambiguous failure, the program should surface the ambiguity, +record it, reconcile it, or hand it to a domain-specific safety mechanism +rather than blindly running the same one-way action again. + +For non-idempotent work, first ask whether another attempt is semantically the +same operation or a new business action. If it is a new business action, a +retry schedule is the wrong boundary even when the failure looks transient. + +##### Notes and caveats + +`Effect.retry` retries typed failures from the error channel according to the +provided policy. It does not inspect the external system to determine whether a +side effect already happened. + +`Schedule.recurs(n)` limits the number of retries after the original attempt. +It is useful for bounding operational cost, but it is not a duplicate-safety +mechanism. + +Timeouts are especially ambiguous for writes. A timeout can mean "the service +did not receive the request," "the service committed the request but the +response was lost," or "the service is still processing the request." + +Do not treat this as advice to never retry writes. Some writes are safe because +the operation is naturally idempotent, transactional, or protected by a +protocol-level duplicate check. The important point is that the safety comes +from the operation boundary, not from `Schedule` itself. + +#### 8.4 Retrying with idempotency keys + +An idempotency key is a stable token that tells a downstream service which +attempts belong to one logical command. This recipe shows where that key belongs +relative to `Effect.retry` and a bounded `Schedule`. + +##### Problem + +The failure mode is using a retry policy without preserving the same key across +attempts. If each attempt uses a different key, the downstream system may treat +them as independent writes. + +The retry policy still matters. A key can prevent duplicate business effects, +but it does not make unbounded retry traffic harmless. Use `Schedule` to keep the +retry delayed, finite, and explicit. + +The important boundary is: create or receive one idempotency key before the +retried write, then reuse that exact key for every attempt made by +`Effect.retry`. + +##### When to use it + +Use this recipe for external writes where the downstream API documents +idempotency-key behavior. Common examples include payment creation, order +submission, shipment creation, ticket creation, and API commands that accept a +header such as `Idempotency-Key`. + +It is most useful for ambiguous failures: timeouts, dropped connections, +gateway errors, rate limits, or service unavailability. In those cases, the +caller may not know whether the first request was committed. Retrying with the +same key asks the server to return or complete the same logical result. + +Keep the retry around the single keyed write. Generate the key outside that +retry boundary, store it with the local command or request record when needed, +and pass it into each attempt. + +##### When not to use it + +Do not generate a fresh idempotency key inside the retried effect. A new key per +attempt usually tells the downstream system that each retry is a new write. + +Do not use this recipe for APIs that ignore idempotency keys or only deduplicate +for a shorter period than your operational workflow requires. + +Do not retry permanent failures such as invalid payloads, authorization +failures, insufficient funds, or business-rule rejections. The key protects +against duplicate execution; it does not make an invalid command valid. + +##### Schedule shape + +For keyed external writes, start with a conservative bounded retry: + +- exponential backoff, so repeated failures slow down +- jitter, so many callers do not retry together +- a finite recurrence limit, so the write cannot retry forever +- a predicate that retries only ambiguous or transient failures + +The example uses `Schedule.exponential("10 millis")` so it terminates quickly. +Production values are usually larger. `Schedule.jittered` randomly adjusts each +delay between 80% and 120%. `Schedule.both(Schedule.recurs(4))` keeps the +schedule finite: both schedules must continue, so the write is retried at most +four times after the original attempt. + +With `Effect.retry`, the write runs once immediately. The same effect is then +re-run only after a typed failure that the predicate allows and only while the +schedule continues. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class CreatePaymentError extends Data.TaggedError("CreatePaymentError")<{ + readonly reason: "Timeout" | "ConnectionReset" | "RateLimited" | "BadGateway" | "InvalidRequest" | "Declined" +}> {} + +interface Payment { + readonly id: string + readonly status: "Created" | "AlreadyCreated" +} + +interface PaymentInput { + readonly customerId: string + readonly amountCents: number + readonly idempotencyKey: string +} + +let attempts = 0 + +const createPayment = (input: PaymentInput): Effect.Effect => + Effect.gen(function*() { + attempts += 1 + yield* Console.log(`payment attempt ${attempts} with key ${input.idempotencyKey}`) + + if (attempts < 3) { + return yield* Effect.fail(new CreatePaymentError({ reason: "Timeout" })) + } + + return { id: "pay_123", status: "Created" } + }) + +const retryKeyedWrite = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const isRetryablePaymentFailure = (error: CreatePaymentError): boolean => { + switch (error.reason) { + case "Timeout": + case "ConnectionReset": + case "RateLimited": + case "BadGateway": + return true + case "InvalidRequest": + case "Declined": + return false + } +} + +const submitPayment = ( + customerId: string, + amountCents: number, + idempotencyKey: string +) => + createPayment({ customerId, amountCents, idempotencyKey }).pipe( + Effect.retry({ + schedule: retryKeyedWrite, + while: isRetryablePaymentFailure + }) + ).pipe( + Effect.tap((payment) => Console.log(`${payment.id} ${payment.status}`)) + ) + +const program = submitPayment("customer-1", 5000, "payment-command-42") + +Effect.runPromise(program) +// Output: +// payment attempt 1 with key payment-command-42 +// payment attempt 2 with key payment-command-42 +// payment attempt 3 with key payment-command-42 +// pay_123 Created +``` + +The `idempotencyKey` is an argument to `submitPayment`, not a value created +inside `createPayment` or inside the retry. Every retry attempt sends the same +`customerId`, `amountCents`, and `idempotencyKey`. + +If the first request reaches the payment provider but the response is lost, a +later attempt with the same key should be treated by that provider as the same +logical payment. `Schedule` controls how many times the client asks again and +how long it waits between attempts. + +##### Variants + +For user-facing writes, keep the retry budget small. The idempotency key +reduces duplicate-write risk, but the user still waits for the retry sequence: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +const userFacingKeyedWrite = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(2)) +) + +const backgroundKeyedWrite = Schedule.exponential("20 millis", 1.5).pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const program = Effect.gen(function*() { + yield* Console.log(`user-facing policy: ${Schedule.isSchedule(userFacingKeyedWrite)}`) + yield* Console.log(`background policy: ${Schedule.isSchedule(backgroundKeyedWrite)}`) +}) + +Effect.runPromise(program) +// Output: +// user-facing policy: true +// background policy: true +``` + +Use the smaller policy when the caller needs a prompt answer. Use the larger +policy for background workers that can tolerate more latency and where the key +is persisted with the job, command, or outbox record. + +If the downstream service returns a "duplicate" or "already processed" response +for the same key, model that as a successful domain result when it represents +the same logical write. Do not turn it into a failure that triggers more +retries. + +##### Notes and caveats + +The idempotency key must identify one logical command. Reusing a key for a +different payload can be rejected by the downstream service or, worse, attach a +new local intent to an old remote result. + +Persist the key before retrying when the operation may outlive the current +fiber, process, or HTTP request. A worker restart should resume the same +logical write with the same key, not invent a new one. + +Check the downstream service's retention window. Some providers remember keys +for hours or days, not forever. Your retry and reconciliation workflow should +fit inside that documented window. + +`Schedule.recurs(4)` means four retries after the original attempt. It does not +mean four total attempts. + +The schedule is still a load-control tool, not the idempotency guarantee. The +duplicate-safety contract comes from the external API honoring the stable key. + +#### 8.5 When not to retry at all + +Sometimes the correct retry policy is no retry. Use that policy when another +attempt would be a new business action, when the failure is permanent, or when +the next step is reconciliation rather than repetition. + +Beginner note: Bounds — a bounded retry policy can still be wrong. Timing and +limits do not make an unsafe operation safe to run again. + +##### The anti-pattern + +Some operations should not receive a retry policy at all. The anti-pattern is +to attach a reasonable-looking `Schedule` to an effect only because the failure +looks temporary: + + + +```ts no-check +import { Console, Effect, Schedule } from "effect" + +let attempts = 0 +let providerCharges = 0 + +const chargeCardOnce = Effect.gen(function*() { + attempts += 1 + providerCharges += 1 + yield* Console.log(`charge attempt ${attempts}; provider charge ${providerCharges}`) + + if (attempts === 1) { + return yield* Effect.fail("response-lost") + } +}) + +const retryTransientFailure = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +// Unsafe: do not attach a generic retry policy to a one-way charge. +const unsafeProgram = chargeCardOnce.pipe( + Effect.retry(retryTransientFailure), + Effect.tap(() => Console.log(`provider charges: ${providerCharges}`)) +) + +Effect.runPromise(unsafeProgram) +``` + +The schedule is finite and delayed, but that does not make the operation safe. +If the charge was accepted and the response was lost, retrying can create a new +business action. + +##### Why it happens + +Retries are often added near the transport boundary. A timeout, connection reset, +or `503` response feels like infrastructure noise, so it is tempting to reuse +the same schedule that works well for reads or duplicate-safe writes. + +That reasoning skips the domain question: what happens if the operation already +partly or fully happened? + +For unsafe writes, the caller may not know whether the remote system committed +the side effect. `Schedule` can decide when to try again, how long to wait, and +when to stop. It cannot decide whether another attempt is the same logical +operation or a second business event. + +Permanent failures are another common source of accidental retries. Validation +errors, malformed requests, authorization failures, missing prerequisites, and +business-rule rejections usually require a different input, different caller +permissions, or an operator fix. Waiting does not change those facts. + +##### Why it is risky + +Retrying the wrong operation converts one failure into several possible +failures. A payment may be charged twice. A shipment may be created twice. An +email or webhook may be delivered multiple times. A state transition may advance +farther than the caller intended. + +It also hides the information the caller needs. If a request is invalid, the +caller should correct it. If credentials are wrong, the caller should +reauthenticate or escalate. If a write has an unknown outcome, the system should +record that ambiguity and reconcile it instead of pretending another attempt is +automatically harmless. + +A retry limit only bounds the number of additional attempts. It does not make an +unsafe operation safe: + +```ts +import { Console, Effect, Schedule } from "effect" + +const boundedButStillUnsafe = Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.recurs(1)) +) + +const program = Console.log(`bounded policy: ${Schedule.isSchedule(boundedButStillUnsafe)}`) + +Effect.runPromise(program) +``` + +This policy allows only one retry after the original attempt, but that one retry +may still be one duplicate too many. + +##### A better approach + +Do not attach `Effect.retry` when the next correct action is correction, +escalation, or reconciliation. + +```ts runnable deterministic +import { Console, Effect, Result } from "effect" + +let providerCharges = 0 + +const submitPaymentOnce = Effect.gen(function*() { + providerCharges += 1 + yield* Console.log(`submitted payment once; provider charge ${providerCharges}`) + return yield* Effect.fail("unknown-payment-outcome") +}) + +const recordForReconciliation = (error: unknown) => Console.log(`recorded for reconciliation: ${String(error)}`) + +const program = Effect.gen(function*() { + const result = yield* Effect.result(submitPaymentOnce) + + if (Result.isFailure(result)) { + return yield* recordForReconciliation(result.failure) + } + + yield* Console.log("payment confirmed") +}) + +Effect.runPromise(program) +// Output: +// submitted payment once; provider charge 1 +// recorded for reconciliation: unknown-payment-outcome +``` + +This program intentionally has no retry schedule around `submitPaymentOnce`. A +failure is treated as an outcome to record and resolve through a safer path. +That path might query the downstream system, notify an operator, expose an +"unknown" status to the caller, or hand the case to a reconciliation worker. + +Use the same shape for permanent errors. Return validation failures to the +caller. Surface authorization failures to the authentication or permission +layer. Send malformed upstream responses to monitoring. Route irrecoverable +infrastructure failures to an operational fallback. In each case, the important +choice is to avoid spending retry attempts on work that cannot become correct by +waiting. + +##### Notes and caveats + +No retry policy is still a policy. It says the operation should be attempted +once and then handled by the domain-specific failure path. + +Avoid retrying when the operation is a non-idempotent write, when the failure is +permanent, when success requires the caller to change the request, or when the +correct next step is human or automated reconciliation. + +This does not mean every write must fail fast forever. Some writes are safe to +retry because they are explicitly duplicate-safe. That safety belongs to the +operation boundary, not to `Schedule`. + +If only part of a workflow is retryable, put the schedule around that smallest +safe part. Do not wrap the whole workflow just because one internal call may +benefit from retry. + +When in doubt, ask what a second attempt means in the business domain. If it +means "try the same logical operation again," a bounded schedule may be +appropriate. If it means "perform another business action," do not retry at that +boundary. + +## Part III — Repeat Recipes + +### 9. Repeat Successful Work + +#### 9.1 Repeat 5 times + +`Effect.repeat` repeats after success. This recipe covers count-bounded +repetition without adding timing or failure recovery. + +##### Problem + +The count is easy to misread. With `Effect.repeat`, the effect runs once before +the schedule is consulted. `Schedule.recurs(5)` therefore means five +recurrences after the original run, for six total executions if every run +succeeds. + +##### When to use it + +Use this when the original effect should execute immediately and a successful +result should be followed by at most five more executions. + +This fits count-bounded sampling, repeating a successful maintenance action a +small number of times, or exercising a successful operation several more times +without adding timing. + +##### When not to use it + +Do not use this when "five times" means five total executions. For five total +executions, use four recurrences: `Schedule.recurs(4)` or +`Effect.repeat({ times: 4 })`. + +Do not use `Effect.repeat` to recover from failures. If the effect fails, +repeating stops and the failure is returned. Use `Effect.retry` when failure +should trigger another attempt. + +##### Schedule shape + +`Schedule.recurs(5)` is the direct schedule shape. Read it as "after the +original successful run, allow five scheduled recurrences." The maximum execution +count is one original run plus five recurrences, for six total executions. + +The schedule output is the recurrence count. When passed directly to +`Effect.repeat`, the repeated program returns the schedule's final output, not +the effect's final value. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let executions = 0 + +const writeMetric = Effect.gen(function*() { + executions += 1 + yield* Console.log(`metric write ${executions}`) + return executions +}) + +const program = writeMetric.pipe( + Effect.repeat(Schedule.recurs(5)), + Effect.tap((recurrenceCount) => Console.log(`schedule output: ${recurrenceCount}; total executions: ${executions}`)) +) + +Effect.runPromise(program) +// Output: +// metric write 1 +// metric write 2 +// metric write 3 +// metric write 4 +// metric write 5 +// metric write 6 +// schedule output: 5; total executions: 6 +``` + +Here `writeMetric` runs once immediately. If it succeeds each time, +`Schedule.recurs(5)` allows five more runs, so the effect can execute six times +total. + +If you want the repeated effect's final successful value instead of the schedule +output, use the options form: + +```ts runnable deterministic +import { Console, Effect } from "effect" + +let sampleNumber = 0 + +const readSample = Effect.gen(function*() { + sampleNumber += 1 + yield* Console.log(`sample ${sampleNumber}`) + return { sampleNumber } +}) + +const finalSample = readSample.pipe( + Effect.repeat({ times: 4 }), + Effect.tap((sample) => Console.log(`final sample: ${sample.sampleNumber}`)) +) + +Effect.runPromise(finalSample) +// Output: +// sample 1 +// sample 2 +// sample 3 +// sample 4 +// sample 5 +// final sample: 5 +``` + +This uses four recurrences for five total executions and returns the final +successful sample. + +##### Variants + +For five total executions, use four recurrences: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let executions = 0 + +const program = Effect.gen(function*() { + executions += 1 + yield* Console.log(`execution ${executions}`) +}).pipe( + Effect.repeat(Schedule.recurs(4)), + Effect.tap(() => Console.log(`total executions: ${executions}`)) +) + +Effect.runPromise(program) +// Output: +// execution 1 +// execution 2 +// execution 3 +// execution 4 +// execution 5 +// total executions: 5 +``` + +Use `Schedule.recurs(5)` when you care about a composable schedule value. Use +`Effect.repeat({ times: 5 })` when you need five recurrences and want the final +successful value of the effect. + +##### Notes and caveats + +`Schedule.recurs(5)` means at most five recurrences. It reaches that count only +if the original run and all repeated runs succeed. + +The original run is not delayed by the schedule. With `Schedule.recurs(5)` alone, +there is no added spacing between recurrences. + +This recipe is only about a fixed recurrence count. Add timing, forever +repetition, or `while` / `until` predicates only when the repeat policy needs +those extra rules. + +#### 9.2 Repeat forever with care + +`Effect.repeat` can run successful work for the lifetime of a process, fiber, or +scope. This recipe focuses on using an explicit owner and spacing policy. + +##### Problem + +An unbounded repeat is easy to express, but it is also easy to make too +aggressive. `Schedule.forever` repeats with no delay. For most operational +loops, use an explicit spacing schedule so each successful run leaves room for +the rest of the system. + +##### When to use it + +Use this for long-lived background work where success means "do it again": +heartbeats, cache refreshes, lightweight health checks, and maintenance loops +owned by a supervised fiber or application scope. + +Use a forever repeat only when the surrounding program has a clear lifetime. The +normal way to stop an unbounded repeat is interruption, cancellation of the +owning fiber, or shutdown of the scope that owns it. + +Beginner note: Bounds — "forever" is acceptable only when ownership is explicit. +For request-response code, prefer a finite schedule or a timeout. + +##### When not to use it + +Do not use a forever repeat for a request-response path. If the effect keeps +succeeding and the schedule is unbounded, the repeated program does not complete +normally. + +Do not use `Schedule.forever` for ordinary background polling unless a tight loop +is intentional. It has zero delay between successful executions and can consume +resources quickly. + +Do not use `Effect.repeat` to recover from failures. A failure stops repetition. +Use `Effect.retry` when failures should trigger another attempt. + +##### Schedule shape + +The smallest forever schedule is `Schedule.forever`. It recurs forever and +outputs the current repetition count: `0`, `1`, `2`, and so on. + +For operational code, prefer a spaced forever schedule: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +const repeatEveryTick = Schedule.spaced("10 millis").pipe( + Schedule.take(2) +) + +const program = Console.log(`spaced policy: ${Schedule.isSchedule(repeatEveryTick)}`) + +Effect.runPromise(program) +// Output: +// spaced policy: true +``` + +`Schedule.spaced(duration)` is also unbounded by default, but it waits for the +duration after each successful run before starting the next recurrence. The first +run still happens immediately; the schedule controls only what happens after a +success. The `Schedule.take(2)` above is only there to keep the example finite. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let refreshes = 0 + +const refreshCache = Effect.gen(function*() { + refreshes += 1 + yield* Console.log(`cache refresh ${refreshes}`) +}) + +const refreshPolicy = Schedule.spaced("10 millis").pipe( + Schedule.tapOutput((repetition) => Console.log(`scheduled repetition ${repetition}`)), + Schedule.take(3) +) + +const program = refreshCache.pipe( + Effect.repeat(refreshPolicy), + Effect.tap((lastRepetition) => Console.log(`stopped demo after repetition ${lastRepetition}`)) +) + +Effect.runPromise(program) +// Output: +// cache refresh 1 +// scheduled repetition 0 +// cache refresh 2 +// scheduled repetition 1 +// cache refresh 3 +// scheduled repetition 2 +// cache refresh 4 +// scheduled repetition 3 +// stopped demo after repetition 3 +``` + +This runs `refreshCache` once immediately. After each success, the schedule +records the repetition count, waits, and allows the next run. The `take` limit +keeps the snippet pasteable and quick to terminate; a real background worker +would normally rely on an owning fiber or scope to stop it. + +Without the demo limit, this shape is intended for long-lived work. It completes +only if `refreshCache` fails, the schedule fails, or the fiber is interrupted. + +##### Variants + +Use `Schedule.forever` only when immediate repetition is deliberate: + +```ts +import { Console, Effect, Schedule } from "effect" + +let drains = 0 + +const drainLocalQueue = Effect.gen(function*() { + drains += 1 + yield* Console.log(`drain pass ${drains}`) +}) + +const program = drainLocalQueue.pipe( + Effect.repeat(Schedule.forever.pipe(Schedule.take(3))), + Effect.tap((lastRepetition) => Console.log(`last repetition: ${lastRepetition}`)) +) + +Effect.runPromise(program) +``` + +This shape has no built-in spacing. It is appropriate only when the effect itself +blocks, waits, or consumes bounded local work. If the effect returns quickly, +prefer `Schedule.spaced`. + +Add schedule-level observability with `Schedule.tapOutput` when the repeat policy +owns the operational signal. Add effect-level logging when the work itself owns +the signal. Keeping the count in the schedule makes it clear that the value is +about recurrence, not about the business result. + +##### Notes and caveats + +`Effect.repeat` runs the effect once before consulting the schedule. A forever +schedule therefore does not delay startup. + +With a raw schedule, `Effect.repeat(schedule)` returns the schedule output if the +schedule completes. A forever schedule does not complete by exhaustion, so this +form is normally used for a long-lived effect rather than for its final value. + +A forever repeat should have an owner. In application code, run it in a +supervised fiber, scoped resource, or runtime structure that will interrupt it +during shutdown. + +Failures are not swallowed. If the repeated effect fails, repetition stops and +the failure is returned. If failure should be logged and then retried, model that +as retry behavior inside the repeated unit or use a retry policy at the +appropriate boundary. + +#### 9.3 Repeat with a pause + +`Effect.repeat` can add a deliberate pause between successful runs. This recipe +covers spacing recurrences without turning the repeat into failure recovery. + +##### Problem + +Without spacing, a successful queue check, cache refresh, or heartbeat can +immediately schedule the next run. Use the schedule to place a fixed pause +between successful recurrences. + +##### When to use it + +Use this when success is the trigger for another run and each recurrence should +wait the same amount of time. + +This is useful when the repeated action is cheap enough to run more than once, +but immediate repetition would be noisy, wasteful, or hard to observe. + +##### When not to use it + +Do not use this to retry failures. `Effect.repeat` repeats after success; if the +effect fails, repetition stops with that failure. Use `Effect.retry` for +failure-driven attempts. + +Do not use this when you need calendar-style periodic timing or alignment to +clock boundaries. A pause between successful recurrences is simpler than a full +periodic schedule. + +Do not leave the schedule unbounded unless the surrounding workflow is intended +to keep running. Add a recurrence limit when the repeated action should stop +after a known number of pauses. + +##### Schedule shape + +The central shape is `Schedule.spaced(duration)`, for example +`Schedule.spaced("10 millis")` in the quick-running snippet below. + +With `Effect.repeat`, the original effect runs once immediately. After a +successful run, the schedule decides whether to recur and how long to wait before +that recurrence. + +`Schedule.spaced("10 millis").pipe(Schedule.take(3))` means three scheduled +recurrences after the original successful run. If every run succeeds, the effect +runs four times total, with a pause before each recurrence. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let refreshes = 0 + +const refreshCache = Effect.gen(function*() { + refreshes += 1 + yield* Console.log(`cache refresh ${refreshes}`) +}) + +const program = refreshCache.pipe( + Effect.repeat(Schedule.spaced("10 millis").pipe(Schedule.take(3))), + Effect.tap((lastRepetition) => Console.log(`last repetition: ${lastRepetition}`)) +) + +Effect.runPromise(program) +// Output: +// cache refresh 1 +// cache refresh 2 +// cache refresh 3 +// cache refresh 4 +// last repetition: 3 +``` + +Here `refreshCache` runs immediately. If it succeeds, Effect waits before the +first recurrence. The same pause is used before each later recurrence, up to +three scheduled recurrences. + +The program returned by `Effect.repeat(schedule)` succeeds with the schedule's +final output. With `Schedule.spaced`, that output is the recurrence count. + +##### Variants + +If you already have a count schedule and want to add a pause to it, use `Schedule.addDelay`: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +const repeatWithPause = Schedule.recurs(3).pipe( + Schedule.addDelay(() => Effect.succeed("10 millis")) +) + +let runs = 0 + +const program = Effect.gen(function*() { + runs += 1 + yield* Console.log(`run ${runs}`) +}).pipe( + Effect.repeat(repeatWithPause), + Effect.tap((lastRepetition) => Console.log(`last repetition: ${lastRepetition}`)) +) + +Effect.runPromise(program) +// Output: +// run 1 +// run 2 +// run 3 +// run 4 +// last repetition: 3 +``` + +This keeps the recurrence count shape explicit and adds the fixed delay to each +scheduled recurrence. + +Use `Schedule.spaced("10 millis").pipe(Schedule.take(3))` when the pause is the +main idea. Use `Schedule.recurs(3).pipe(Schedule.addDelay(...))` when you want to +start from a count policy and attach timing to it. + +##### Notes and caveats + +The pause is not before the first run. The first evaluation of the effect +happens immediately; the schedule controls only later recurrences. + +The pause happens only after success. A failure from the repeated effect stops +the repeat and returns the failure. + +`Schedule.spaced` by itself is unbounded. Pair it with a limit, another stopping +rule, or an enclosing lifetime when the workflow must end. + +`Schedule.addDelay` adds to any delay the base schedule already chose. With +`Schedule.recurs(3)`, this effectively adds the fixed pause to each recurrence. + +#### 9.4 Repeat until a condition becomes true + +`Effect.repeat` can keep running successful work until the latest successful +value satisfies a condition. This recipe uses that value as the schedule input. + +##### Problem + +The condition is checked after each successful run, because the first run +happens before the schedule is consulted. When the condition is already true +after the first run, there are no recurrences. + +##### When to use it + +Use this when success is not enough by itself; the successful value must also +indicate that the repeated work is complete. + +This is useful for short repeat loops such as reading a local status value, +advancing a small workflow step, or sampling a value until it reaches a desired +state. + +##### When not to use it + +Do not use this to retry failures. If the effect fails, `Effect.repeat` stops +and returns that failure. Use `Effect.retry` when failure should trigger another +attempt. + +Do not use this as a full polling recipe for external systems with budgets, +observability, and terminal-state handling. Those concerns usually need +additional schedules, limits, and domain-specific result handling. + +Do not leave the repeat unbounded unless the condition is guaranteed by the +surrounding workflow or the fiber has a clear owner that can interrupt it. + +##### Schedule shape + +Use a schedule whose input is the effect's successful output, then continue while +the condition is not yet true. `Schedule.identity()` makes each +successful `Result` both the schedule input and output. `Schedule.while` receives +schedule metadata after a successful run. Returning `true` allows another +recurrence; returning `false` stops the repeat. + +Because the predicate is `!isDone(input)`, the repeat continues while the latest +successful value is not done and stops as soon as a successful value is done. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type JobStatus = + | { readonly state: "running"; readonly progress: number } + | { readonly state: "ready"; readonly resultId: string } + +let checks = 0 + +const checkJob = Effect.gen(function*() { + checks += 1 + + const status: JobStatus = checks < 3 + ? { state: "running", progress: checks * 50 } + : { state: "ready", resultId: "result-1" } + + yield* Console.log(`check ${checks}: ${status.state}`) + return status +}) + +const untilReady = Schedule.identity().pipe( + Schedule.while(({ input }) => input.state !== "ready") +) + +const finalStatus = checkJob.pipe( + Effect.repeat(untilReady), + Effect.tap((status) => Console.log(`final state: ${status.state}`)) +) + +Effect.runPromise(finalStatus) +// Output: +// check 1: running +// check 2: running +// check 3: ready +// final state: ready +``` + +`checkJob` runs once immediately. If it succeeds with `{ state: "ready", ... }`, +the schedule stops and `finalStatus` succeeds with that ready status. If it +succeeds with `{ state: "running", ... }`, the schedule allows another +recurrence. + +The returned value is the schedule's final output. With +`Schedule.identity()`, that output is the successful `JobStatus` that +made the condition false. + +##### Variants + +Add spacing when the next recurrence should wait after each non-terminal success: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type JobStatus = + | { readonly state: "running"; readonly progress: number } + | { readonly state: "ready"; readonly resultId: string } + +let checks = 0 + +const checkJob = Effect.gen(function*() { + checks += 1 + const status: JobStatus = checks < 2 + ? { state: "running", progress: 50 } + : { state: "ready", resultId: "result-2" } + yield* Console.log(`check ${checks}: ${status.state}`) + return status +}) + +const untilReadyWithPause = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state !== "ready") +) + +const finalStatus = checkJob.pipe( + Effect.repeat(untilReadyWithPause), + Effect.tap((status) => Console.log(`final state: ${status.state}`)) +) + +Effect.runPromise(finalStatus) +// Output: +// check 1: running +// check 2: ready +// final state: ready +``` + +`Schedule.spaced("10 millis")` supplies the pause. +`Schedule.satisfiesInputType()` tells TypeScript that the schedule +will be stepped with successful `JobStatus` values. `Schedule.passthrough` keeps +that `JobStatus` as the schedule output, so the repeated effect still returns +the final status rather than the recurrence count. + +If you do not need to keep the final successful value as the schedule output, you +can omit `Schedule.identity` or `Schedule.passthrough`. When a direct count or +timing schedule has `unknown` input and the predicate reads the successful +output, constrain the input first with `Schedule.satisfiesInputType()`, +then use `Schedule.while`. + +##### Notes and caveats + +The condition is checked only after a successful run. A failure from the effect +does not reach the schedule predicate; it stops the repeat immediately. + +This is "repeat until success output is good enough," not "retry until success." +The repeated effect must succeed for the condition to be inspected. + +The first run is not delayed by the schedule. Any spacing applies only before +later recurrences. + +Without a limit or external interruption, a condition that never becomes true can +repeat forever. Add a recurrence limit, time budget, or owning fiber lifetime +when that is not acceptable. + +#### 9.5 Repeat while work remains to be done + +`Effect.repeat` can keep draining work while each successful result says more +work remains. This recipe focuses on continuation signals such as remaining +counts, cursors, or `hasMore` flags. + +##### Problem + +The repeated effect should advance one unit of work and return the signal that +decides whether another run is needed. A queue drain may process one batch and +return the number of remaining messages; a paginated import may fetch one page +and return whether there is another page. + +##### When to use it + +Use this when each successful run advances external state and returns a +continuation signal such as `remaining > 0`, `hasMore: true`, or +`nextCursor !== undefined`. + +This shape fits queue drains, local backlog processing, batch cleanup, and +page-by-page ingestion where every run should ask the work source for the next +unit. + +##### When not to use it + +Do not use this to recover from failures. `Effect.repeat` repeats after success; +if the effect fails, repetition stops with that failure. Use `Effect.retry` when +failures should trigger another attempt. + +Do not use this when there is no natural work-complete signal in the successful +result. If the loop is meant to run for the lifetime of a process, use an +explicitly long-lived repeat policy instead. + +Do not use this as a deep periodic polling recipe. This section is about draining +known work until the successful output says the drain is complete. + +##### Schedule shape + +The central shape is an unbounded schedule guarded by the latest successful +output. With `Effect.repeat(schedule)`, the successful value produced by the +effect becomes the schedule input. `Schedule.while` receives schedule metadata, +so `metadata.input` is the latest successful result. + +If the predicate returns `true`, the schedule allows another recurrence. If it +returns `false`, the repeat stops. + +##### Example + +```ts +import { Console, Effect, Schedule } from "effect" + +interface QueueDrainResult { + readonly processed: number + readonly remaining: number +} + +let remainingMessages = 5 + +const drainOneBatch = Effect.gen(function*() { + const processed = Math.min(2, remainingMessages) + remainingMessages -= processed + + const result: QueueDrainResult = { + processed, + remaining: remainingMessages + } + + yield* Console.log(`processed ${result.processed}; remaining ${result.remaining}`) + return result +}) + +const whileQueueHasWork = Schedule.forever.pipe( + Schedule.satisfiesInputType(), + Schedule.while(({ input }) => input.remaining > 0) +) + +const drainQueue = drainOneBatch.pipe( + Effect.repeat(whileQueueHasWork), + Effect.tap((lastRepetition) => Console.log(`last repetition: ${lastRepetition}`)) +) + +Effect.runPromise(drainQueue) +``` + +`drainOneBatch` runs once immediately. If it succeeds with `remaining > 0`, the +schedule permits another batch drain. When a successful batch reports +`remaining === 0`, the schedule stops and `drainQueue` completes. + +The repeated program succeeds with the schedule output, not with the last +`QueueDrainResult`. With `Schedule.forever`, that output is the recurrence count. + +##### Variants + +Add a small pause between successful batches when the downstream system needs +breathing room: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +interface PageResult { + readonly imported: number + readonly hasMore: boolean +} + +let nextPage = 1 + +const importNextPage = Effect.gen(function*() { + const result: PageResult = { + imported: 10, + hasMore: nextPage < 3 + } + yield* Console.log(`imported page ${nextPage}`) + nextPage += 1 + return result +}) + +const whilePagesRemain = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.while(({ input }) => input.hasMore) +) + +const importAllAvailablePages = importNextPage.pipe( + Effect.repeat(whilePagesRemain), + Effect.tap((lastRepetition) => Console.log(`last repetition: ${lastRepetition}`)) +) + +Effect.runPromise(importAllAvailablePages) +// Output: +// imported page 1 +// imported page 2 +// imported page 3 +// last repetition: 2 +``` + +Use `Schedule.forever.pipe(Schedule.satisfiesInputType(), Schedule.while(...))` +when the next run should start immediately and the predicate reads the successful +output. Use +`Schedule.spaced(duration).pipe(Schedule.satisfiesInputType(), Schedule.while(...))` +when each successful run should leave a deliberate pause before the next unit of +work. + +If you also need a hard safety limit, combine the continuation predicate with a +bounded schedule: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +interface QueueDrainResult { + readonly processed: number + readonly remaining: number +} + +const atMostOneHundredMoreBatches = Schedule.recurs(100).pipe( + Schedule.satisfiesInputType(), + Schedule.while(({ input }) => input.remaining > 0) +) + +const program = Effect.gen(function*() { + yield* Console.log(`bounded drain policy: ${Schedule.isSchedule(atMostOneHundredMoreBatches)}`) +}) + +Effect.runPromise(program) +// Output: +// bounded drain policy: true +``` + +This still stops when the queue reports no remaining work, but it also stops +after one hundred scheduled recurrences even if the result keeps saying that work +remains. + +##### Notes and caveats + +The first run is not controlled by the schedule. `Effect.repeat` evaluates the +effect once, then passes that successful output to the schedule to decide whether +to run again. + +The predicate sees successful outputs only. Failures do not become schedule +inputs for `Effect.repeat`; a failure from the repeated effect stops the repeat. + +Make sure the repeated effect advances the drain. If every successful run returns +the same `remaining` or `hasMore` value without consuming work, the schedule can +keep recurring forever. + +When you care about the final business result, model that explicitly in the +repeated effect or surrounding workflow. The raw `Effect.repeat(schedule)` result +is the schedule's final output. + +### 10. Periodic and Spaced Repeat + +#### 10.1 Run every minute + +Use this when successful background work should run now and then recur on a +one-minute cadence. + +##### Problem + +A cache refresh, metrics publisher, or local-state check needs an immediate +first run and later successful recurrences once per minute. + +##### When to use it + +Use `Schedule.fixed("1 minute")` when minute-level cadence matters and +second-level freshness would be unnecessary load. + +This fits background work owned by a long-lived process, scope, or supervised +fiber. + +##### When not to use it + +Do not use this for failure recovery. If the effect fails, `Effect.repeat` +stops with that failure. + +Do not use an unbounded minute loop in a request-response path that needs to +complete. + +Do not use this as a cron replacement. A fixed one-minute interval is not the +same as "at the top of every minute" or "only during business hours." + +##### Schedule shape + +The core schedule is `Schedule.fixed("1 minute")`. + +`fixed` schedules recurrences against interval boundaries. If a run takes +longer than a minute, the next recurrence may run immediately, but missed runs +do not pile up. Use `Schedule.spaced("1 minute")` when the gap after completion +is what matters. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let refreshes = 0 + +const refreshCache = Effect.gen(function*() { + refreshes += 1 + yield* Console.log(`cache refresh ${refreshes}`) +}) + +const loop = refreshCache.pipe( + Effect.repeat(Schedule.fixed("1 minute")) +) + +const program = loop.pipe( + Effect.timeoutOrElse({ + duration: "50 millis", + orElse: () => Console.log(`demo stopped after ${refreshes} refresh`) + }) +) + +Effect.runPromise(program) +// Output: +// cache refresh 1 +// demo stopped after 1 refresh +``` + +The timeout keeps the example quick while still using the real one-minute +schedule. + +##### Variants + +Use `Schedule.spaced("1 minute")` when every completed run should be followed +by one quiet minute. Add `Schedule.take(n)` when a diagnostic or test should +stop after a fixed number of recurrences. + +##### Notes and caveats + +The schedule does not delay the first execution. It controls only later +successful recurrences. + +`Schedule.fixed("1 minute")` runs one recurrence at a time. It does not start +concurrent catch-up runs. + +If transient failures should not stop the loop, handle retry or recovery inside +the repeated effect before applying the periodic repeat. + +#### 10.2 Run every hour + +Use this when successful background work should run now and then recur on an +hourly cadence. + +##### Problem + +Slow-moving reference data, local compaction, summary metrics, or another +low-frequency task needs an immediate first run followed by successful hourly +recurrences. + +##### When to use it + +Use `Schedule.fixed("1 hour")` when the action should stay on a regular +hourly cadence. + +This fits background work owned by a long-lived process, scope, or supervised +fiber. + +##### When not to use it + +Do not use `Effect.repeat` as failure recovery. If the action fails, the +repeated effect fails unless you handle or retry that failure inside the action. + +Do not use this for calendar-aware scheduling, such as "run at the top of every +hour" or "run only during business hours." This recipe is about a periodic +one-hour interval. + +Do not use a fixed hourly cadence when every run must be followed by one quiet +hour after it completes. Use `Schedule.spaced("1 hour")` for that shape. + +##### Schedule shape + +`Schedule.fixed("1 hour")` recurs on a fixed interval and outputs the number +of repetitions so far. + +With `Effect.repeat`, the first run happens immediately. The schedule controls +successful recurrences after that first run. + +If a run takes longer than one hour, the next run starts immediately when the +current run completes, but missed runs do not pile up. + +By contrast, `Schedule.spaced("1 hour")` waits one full hour after each +successful run completes. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let syncs = 0 + +const syncReferenceData = Effect.gen(function*() { + syncs += 1 + yield* Console.log(`reference-data sync ${syncs}`) +}) + +const loop = syncReferenceData.pipe( + Effect.repeat(Schedule.fixed("1 hour")) +) + +const program = loop.pipe( + Effect.timeoutOrElse({ + duration: "50 millis", + orElse: () => Console.log(`demo stopped after ${syncs} sync`) + }) +) + +Effect.runPromise(program) +// Output: +// reference-data sync 1 +// demo stopped after 1 sync +``` + +The timeout keeps the example quick. The hourly schedule itself is unbounded +and should be owned by the surrounding application. + +##### Variants + +Use `Schedule.spaced("1 hour")` when the requirement is "wait one hour after +finishing" rather than "keep an hourly cadence." Use a named schedule value for +shared hourly policies so the duration is not scattered through background +workers. + +##### Notes and caveats + +`Schedule.fixed("1 hour")` does not run actions concurrently by itself. A slow +run delays the next run. + +Hourly background work often touches caches, snapshots, indexes, or external +state. Decide whether duplicate successful runs are harmless before making the +loop long-lived. + +If transient failures should not stop the hourly loop, handle recovery inside +the repeated action before applying the periodic repeat. + +#### 10.3 Enforce a pause between iterations + +Use `Schedule.spaced` when each successful repeat should wait before the next +iteration starts. + +##### Problem + +After a refresh, heartbeat, or lightweight poll succeeds, an immediate recurrence +can be too aggressive. The loop should run again only after a known pause. + +##### When to use it + +Use this when the gap after completed work matters more than wall-clock +alignment. + +`Schedule.spaced(duration)` runs the effect once immediately, then waits for the +duration after each successful run before allowing another recurrence. + +##### When not to use it + +Do not use this to retry failures. `Effect.repeat` is success-driven; a failure +from the effect stops the repeat. Use `Effect.retry` for failure-driven attempts. + +Do not use this for fixed-rate cadence such as "run on each one-second +boundary." Use `Schedule.fixed(duration)` for interval alignment. + +Do not use this when the first run itself must be delayed. The schedule controls +only recurrences after the first evaluation. + +##### Schedule shape + +The central shape is `Schedule.spaced("2 seconds")`: each scheduled recurrence +is separated from the previous successful run by a two-second pause. + +This is different from `Schedule.fixed("2 seconds")`. `fixed` schedules recurrences against interval boundaries. If the effect takes longer than the interval, the next recurrence may happen immediately so the schedule can continue from the current time. `spaced` still waits the requested duration after the run completes. + +When the repeat should stop after a known number of scheduled recurrences, add `Schedule.take`, as in `Schedule.spaced("2 seconds").pipe(Schedule.take(3))`. This allows three scheduled recurrences after the original successful run. If all runs succeed, the effect runs four times total. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let run = 0 + +const refresh = Effect.gen(function*() { + run += 1 + yield* Console.log(`refresh ${run}`) + return run +}) + +const program = Effect.gen(function*() { + const finalRecurrence = yield* refresh.pipe( + Effect.repeat(Schedule.spaced("10 millis").pipe(Schedule.take(2))) + ) + yield* Console.log(`repeat returned schedule output ${finalRecurrence}`) +}) + +Effect.runPromise(program) +// Output: +// refresh 1 +// refresh 2 +// refresh 3 +// repeat returned schedule output 2 +``` + +This prints three refreshes: the initial run plus two scheduled recurrences. The +short delay keeps the example quick while still showing that the schedule waits +between successful runs. + +##### Variants + +Name the schedule when the same spacing policy is shared across a workflow, for +example `const everyTwoSeconds = Schedule.spaced("2 seconds").pipe(Schedule.take(5))`. + +Use `Schedule.spaced(duration)` for "wait after completed work." Use +`Schedule.fixed(duration)` for "target this periodic interval." + +##### Notes and caveats + +The pause is not added before the first run. The schedule controls only recurrences after the first successful evaluation. + +The pause happens only after success. A failure from the repeated effect stops the repeat and returns the failure. + +`Schedule.spaced` is unbounded by itself. Combine it with `Schedule.take`, another stopping rule, or an enclosing lifetime when the workflow must end. + +The repeated program succeeds with the schedule's final output when the schedule completes. With `Schedule.spaced`, that output is the recurrence count. + +#### 10.4 Slow down a tight worker loop + +Use `Schedule.spaced` when a worker can complete successfully without finding +work and should not immediately check again. + +##### Problem + +An empty queue, inbox, or table can make a worker complete almost instantly. If +that successful "nothing available" result repeats without a pause, the worker +spins and burns CPU. + +##### When to use it + +Use this when each successful worker iteration should leave a deliberate pause +before the next check. + +This fits simple polling workers where "no work available" is a successful +observation, not an error. + +##### When not to use it + +Do not use this to recover from failures. `Effect.repeat` stops when the worker +effect fails. Use `Effect.retry` when failure should trigger the next attempt. + +Do not use this when the worker must run on clock-aligned boundaries. +`Schedule.spaced` waits after a successful run completes, so start-to-start time +includes the work duration plus the spacing. + +Do not treat this as a complete load-shedding policy. This recipe only prevents +a fast successful loop from spinning. + +##### Schedule shape + +The central shape is `Schedule.spaced("250 millis")`. + +With `Effect.repeat`, the worker runs once immediately. After a successful iteration, `Schedule.spaced("250 millis")` waits 250 milliseconds before allowing the next recurrence. + +The spacing is after success, not before the first run. If the worker fails, the +repeat fails immediately. + +For a bounded worker loop, combine the spacing with `Schedule.take`: + +`Schedule.spaced("250 millis").pipe(Schedule.take(100))` permits 100 scheduled +recurrences after the initial successful run. If every iteration succeeds, the +worker runs 101 times total. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let checks = 0 + +const pollOnce = Effect.gen(function*() { + checks += 1 + yield* Console.log(`queue check ${checks}: empty`) + return "empty" as const +}) + +const program = Effect.gen(function*() { + const finalRecurrence = yield* pollOnce.pipe( + Effect.repeat(Schedule.spaced("10 millis").pipe(Schedule.take(3))) + ) + yield* Console.log(`worker stopped after recurrence ${finalRecurrence}`) +}) + +Effect.runPromise(program) +// Output: +// queue check 1: empty +// queue check 2: empty +// queue check 3: empty +// queue check 4: empty +// worker stopped after recurrence 3 +``` + +The worker prints four checks: one initial check and three scheduled +recurrences. In a real worker, use a production interval such as 250 +milliseconds or one second instead of the short example delay. + +##### Variants + +Use a named schedule when the worker policy is shared, for example +`const workerSpacing = Schedule.spaced("1 second").pipe(Schedule.take(60))`. +This keeps the worker from spinning and gives finite jobs a clear limit. + +Use a shorter spacing when fast pickup matters and the empty loop is still too expensive. Use a longer spacing when empty checks are cheap to defer and CPU quietness matters more than immediate pickup. + +##### Notes and caveats + +`Schedule.spaced` is unbounded by itself. For finite examples, tests, command-line jobs, or short-lived workers, add `Schedule.take` or another stopping rule. + +The pause is controlled by successful completion of the worker effect. A long-running iteration is not interrupted or shortened by the schedule. + +A failure from the worker stops the repeat. The schedule does not turn failures into delayed successes. + +The output of the repeated program is the schedule's final output when the schedule completes. With `Schedule.spaced`, that output is the recurrence count. + +#### 10.5 Use spacing to smooth resource usage + +Use `Schedule.spaced` when a successful repeat loop should spread resource use +over time instead of producing bursts. + +##### Problem + +Each run may consume CPU, database connections, queue visibility checks, cache +bandwidth, file handles, or external API quota. If the next iteration starts +immediately after each success, the loop can create bursts of usage even when every +individual run is correct. + +##### When to use it + +Use this when the loop should keep making progress, but each successful +iteration should leave a predictable gap before the next one starts. + +This is useful for polling, periodic cleanup, small batch processing, and maintenance work where the exact wall-clock boundary is less important than avoiding back-to-back successful runs. + +Use `Schedule.spaced(duration)` when the policy is "after a successful run completes, wait this long before the next recurrence." + +##### When not to use it + +Do not use this to retry failures. `Effect.repeat` stops when the effect fails. +Use `Effect.retry` for failure-driven recovery. + +Do not use this as a full rate limiter. Spacing one repeat loop smooths that loop's own resource usage, but it does not coordinate with other fibers, processes, users, or services. + +Do not use this when work must run on fixed interval boundaries. `Schedule.spaced` waits after completion, so the time between starts includes both the work duration and the configured spacing. Use `Schedule.fixed(duration)` for fixed-rate cadence. + +##### Schedule shape + +The central shape is `Schedule.spaced("1 second").pipe(Schedule.take(30))`. +`Schedule.spaced("1 second")` waits one second after each successful iteration +before allowing the next recurrence. + +`Schedule.take(30)` bounds the repeat to 30 scheduled recurrences after the initial successful run. If every run succeeds, the effect runs 31 times total. + +Together, the schedule says: run now, then keep repeating after success with a fixed gap between completed work items, and stop after a known recurrence limit. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let batch = 0 + +const processOneBatch = Effect.gen(function*() { + batch += 1 + yield* Console.log(`processed batch ${batch}`) + return batch +}) + +const smoothBatchSchedule = Schedule.spaced("10 millis").pipe( + Schedule.take(3) +) + +const program = Effect.gen(function*() { + const finalRecurrence = yield* processOneBatch.pipe( + Effect.repeat(smoothBatchSchedule) + ) + yield* Console.log(`smoothing run stopped after recurrence ${finalRecurrence}`) +}) + +Effect.runPromise(program) +// Output: +// processed batch 1 +// processed batch 2 +// processed batch 3 +// processed batch 4 +// smoothing run stopped after recurrence 3 +``` + +The example prints four batch runs with a short pause between successful +recurrences. Use a larger duration when smoothing real CPU, connection, cache, +or API pressure. + +##### Variants + +Use shorter spacing when responsiveness matters and each iteration is cheap. +Use longer spacing when repeated work competes with interactive traffic, keeps +connections open, or causes visible load on a dependency. + +For finite jobs, keep the recurrence limit explicit with `Schedule.take` or +another stopping rule. + +For long-lived services, the schedule can be unbounded, but the fiber running the repeat should still be tied to the service lifetime. + +##### Notes and caveats + +The spacing is applied after successful completion, not before the first run. + +The duration of the work is not hidden by the schedule. If one iteration takes three seconds and the spacing is one second, the next start is roughly four seconds after the previous start. + +Spacing smooths only this repeat loop. It does not provide a global request budget, distributed coordination, or fairness across callers. + +Choose a spacing that matches the resource being protected. A database maintenance loop, a local cache refresh, and an external API poll usually need different gaps. + +`Schedule.spaced` is unbounded by itself. Add `Schedule.take` or another stopping rule when the repeat belongs to a finite operation, test, or command-line program. + +### 11. Repeat with Limits + +#### 11.1 Repeat at most N times + +Use this when a successful effect needs a count limit and the off-by-one +behavior of `Effect.repeat` must stay explicit. + +##### Problem + +The requirement is "run once now, then allow at most `N` more successful +recurrences." + +With `Effect.repeat`, the effect runs once before the schedule is consulted. +`Schedule.recurs(n)` therefore means "after the original successful run, allow +at most `n` recurrences." + +##### When to use it + +Use this when the repeat limit is a recurrence budget: one original run now, +followed by at most `N` more successful runs. + +This fits bounded sampling, short repeated maintenance actions, and repeat +loops where the count itself is the policy. + +##### When not to use it + +Do not use `Schedule.recurs(n)` unchanged when the requirement counts total +executions. If you want at most `N` total executions, use +`Schedule.recurs(N - 1)` for positive `N`. + +Do not use repeat limits to recover from failures. `Effect.repeat` repeats only +after success; if the effect fails, repetition stops with that failure. + +##### Schedule shape + +The count belongs to the scheduled recurrences, not to the original run: + +| Requirement | Schedule | +| ----------------------------------------- | ------------------------ | +| original run only | `Schedule.recurs(0)` | +| original run plus at most 1 recurrence | `Schedule.recurs(1)` | +| original run plus at most `N` recurrences | `Schedule.recurs(N)` | +| at most `N` total executions, for `N > 0` | `Schedule.recurs(N - 1)` | + +`Schedule.recurs(n)` outputs a zero-based recurrence count. When used directly +with `Effect.repeat`, the repeated program returns the final schedule output. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +let runs = 0 + +const sample = Effect.gen(function*() { + runs += 1 + yield* Console.log(`run ${runs}`) + return runs +}) + +const program = Effect.gen(function*() { + const scheduleOutput = yield* sample.pipe( + Effect.repeat(Schedule.recurs(3)) + ) + + yield* Console.log(`total executions: ${runs}`) + yield* Console.log(`schedule output: ${scheduleOutput}`) +}) + +Effect.runPromise(program) +// Output: +// run 1 +// run 2 +// run 3 +// run 4 +// total executions: 4 +// schedule output: 3 +``` + +This can run four times total: one original run plus three scheduled +recurrences. + +##### Variants + +When the only policy is a repeat count and you want the final successful value +of the effect, use `Effect.repeat({ times: n })`. `times` also counts +recurrences after the original run. + +Use `Schedule.recurs(n)` when you want a first-class schedule value that can be +named, reused, or composed with other schedule combinators. + +##### Notes and caveats + +`Schedule.recurs(n)` allows at most `n` recurrences. It reaches that limit only +if the original run and every repeated run succeed. + +The original run is not part of the schedule count. This is the main +off-by-one point to check when translating requirements. + +Passing `Schedule.recurs(n)` directly to `Effect.repeat` returns the schedule's +final output. Use `Effect.repeat({ times: n })` when the final value of the +effect is the value you want to keep. + +#### 11.2 Repeat only within a time budget + +Use this when successful recurrences should stay open only for an elapsed time +budget. + +##### Problem + +A worker needs to poll during a warm-up window, refresh a cache briefly after a +trigger, or sample an operation for at most a few seconds. + +The effect should run immediately, then allow later successful recurrences only +while the elapsed budget remains open. + +##### When to use it + +Use this when the limit is naturally expressed as elapsed schedule time: +"repeat for up to 10 seconds" or "keep checking during this 1 minute window." + +This is a good fit when each successful run may allow another recurrence, but +the loop must not remain open forever. + +##### When not to use it + +Do not use this to retry failures. `Effect.repeat` repeats after success; if +the effect fails, repetition stops with that failure. + +Do not use a schedule budget as a hard timeout for a run that is already in +progress. The schedule is checked between successful runs; it does not interrupt +the currently running effect. + +Do not use this when the limit is purely a count. Use `Schedule.recurs(n)` for +that, or combine count and time when both constraints matter. + +##### Schedule shape + +Combine a cadence with `Schedule.during(duration)`: + +`Schedule.spaced("1 second").pipe(Schedule.both(Schedule.during("10 seconds")))` + +`Schedule.spaced` chooses the delay between successful recurrences. +`Schedule.during` tracks elapsed schedule time. `Schedule.both` requires both +schedules to continue, so the repeat stops when the budget is exhausted. + +##### Example + +```ts runnable +import { Console, Effect, Schedule } from "effect" + +let polls = 0 + +const pollOnce = Effect.gen(function*() { + polls += 1 + yield* Console.log(`poll ${polls}`) +}) + +const repeatWithinBudget = Schedule.spaced("20 millis").pipe( + Schedule.both(Schedule.during("75 millis")) +) + +const program = Effect.gen(function*() { + yield* pollOnce.pipe(Effect.repeat(repeatWithinBudget)) + + yield* Console.log(`total polls: ${polls}`) +}) + +Effect.runPromise(program) +// Output may vary: elapsed timing can cross the budget boundary differently under load +// poll 1 +// poll 2 +// poll 3 +// poll 4 +// poll 5 +// total polls: 5 +``` + +The example uses millisecond durations so it terminates quickly. The same shape +works with larger production budgets. + +##### Variants + +Add a count cap when the repeat should stop at whichever limit is reached first: + +Use the same cadence and budget, then add +`Schedule.both(Schedule.recurs(20))`. + +If each individual run also needs a hard duration limit, apply +`Effect.timeout` to the repeated effect itself. The schedule budget still limits +only the recurrence window after successful runs. + +##### Notes and caveats + +The first run is not delayed. `Effect.repeat` evaluates the effect once, then +uses the schedule for later successful recurrences. + +`Schedule.during(duration)` is a stopping condition, not a cadence. Combine it +with `Schedule.spaced`, `Schedule.fixed`, or another delay-producing schedule. + +The elapsed budget is checked between successful runs. It is not a substitute +for `Effect.timeout` when a single run must be interrupted after a duration. + +Because `Schedule.both` combines outputs, the resulting schedule output is a +tuple. Keep that output internal when callers only care that the loop finished. + +#### 11.3 Repeat until a threshold is reached + +Use this recipe when the successful output of each run decides whether repetition +should continue. + +##### Problem + +A progress read, backlog sample, score refresh, or other domain check returns a +successful value that can be compared with a threshold. + +With `Effect.repeat`, the effect runs once before the schedule is consulted. +After each successful run, the successful output becomes the schedule input. +That is the value `Schedule.while` inspects when it decides whether another +recurrence is allowed. + +##### When to use it + +Use this when the repeated operation succeeds before the whole workflow is done, +and the successful output tells you how close the workflow is to completion. + +Typical examples include sampling progress until it reaches `100`, processing +batches until the backlog falls below a target, or refreshing a score until it +is at least a required minimum. + +##### When not to use it + +Do not use this to retry failures. If the effect fails, `Effect.repeat` stops +with that failure before the schedule predicate can inspect anything. + +Do not use this when the threshold is not visible in the successful output. In +that case, make the effect return the domain measurement you need, or move the +decision into the effect itself. + +Do not use an unbounded threshold loop unless the threshold is guaranteed by the +surrounding workflow or the fiber has a clear lifetime owner. + +##### Schedule shape + +Make the successful output the schedule input, preserve it as the schedule +output, then continue while it is still below the threshold. + +`Schedule.while` receives schedule metadata after a successful run. Returning +`true` allows another recurrence. Returning `false` stops the repeat. + +The predicate above therefore means "repeat while the latest successful +`Progress` value is still below `100`." When a successful run returns +`percent >= 100`, the repeat stops. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +interface Progress { + readonly percent: number +} + +let percent = 0 + +const readProgress = Effect.gen(function*() { + percent = Math.min(percent + 40, 100) + yield* Console.log(`progress: ${percent}%`) + return { percent } +}) + +const untilComplete = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.percent < 100) +) + +const program = Effect.gen(function*() { + const finalProgress = yield* readProgress.pipe( + Effect.repeat(untilComplete) + ) + + yield* Console.log(`final progress: ${finalProgress.percent}%`) +}) + +Effect.runPromise(program) +// Output: +// progress: 40% +// progress: 80% +// progress: 100% +// final progress: 100% +``` + +`readProgress` runs once immediately. If it succeeds with `percent >= 100`, no +recurrence is scheduled. If it succeeds with `percent < 100`, the schedule +allows another run. + +Because the schedule uses `Schedule.passthrough`, the repeated program succeeds +with the final successful `Progress` value that stopped the loop. + +##### Variants + +Add a recurrence limit or a pause when the threshold may take time to appear: + +Use the same threshold schedule, then compose it with +`Schedule.bothLeft(Schedule.recurs(20).pipe(Schedule.satisfiesInputType()))`. + +The repeat then stops when either a successful output reaches `percent >= 100` +or twenty scheduled recurrences have been allowed. + +##### Notes and caveats + +The threshold predicate inspects only successful outputs, after each successful +run. It does not see failures. + +The first run is not delayed by the schedule. Delays apply only before later +recurrences. + +Use `<` for "repeat while below the threshold" and `<=` when the threshold must +be strictly exceeded. Make the boundary explicit in the predicate. + +When composing a timing or count schedule with `Schedule.while`, constrain the +input type with `Schedule.satisfiesInputType()` before reading +`metadata.input`, then use `Schedule.passthrough` when callers need the final +successful value. + +#### 11.4 Repeat until output becomes stable + +Use this recipe when repeated successful observations should stop once a named +stability comparison says the output is unchanged. + +##### Problem + +A read model may be stable when two consecutive reads have the same version. A +cache snapshot may be stable when its checksum stops changing. The repeat +should compare successful observations and stop when the named comparison says +the value is stable. + +##### When to use it + +Use this when success means "I observed the current state", not necessarily +"the workflow is finished". + +The schedule should carry enough state to compare the latest successful output +with the previous successful output. The stability predicate should be explicit: +same version, same checksum, same count, or another domain comparison that +means "unchanged" for this workflow. + +##### When not to use it + +Do not use this to retry failures. `Effect.repeat` repeats after successful +results; if the effect fails, repetition stops with that failure. + +Do not use this when one unchanged observation is too weak a signal. Some +systems can return the same value briefly and then change again. In that case, +require a stable streak or combine the stability check with a delay and a count +cap. + +Do not hide an expensive or fuzzy comparison inside the schedule without naming +the criterion. Readers should be able to tell exactly what "stable" means. + +##### Schedule shape + +Start with a cadence, constrain it to accept the successful observation as +input, preserve that observation as output, then reduce it into comparison +state. + +`Schedule.passthrough` keeps the latest successful observation as the schedule +output. `Schedule.reduce` remembers the previous observation and computes a +stability state. `Schedule.while` stops when that state is stable. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +interface Snapshot { + readonly version: string + readonly itemCount: number +} + +interface StabilityState { + readonly previous: Snapshot | undefined + readonly current: Snapshot | undefined + readonly stable: boolean +} + +const snapshots: ReadonlyArray = [ + { version: "v1", itemCount: 10 }, + { version: "v2", itemCount: 12 }, + { version: "v2", itemCount: 12 } +] + +let index = 0 + +const readSnapshot = Effect.gen(function*() { + const lastSnapshot = snapshots[snapshots.length - 1]! + const snapshot = snapshots[index] ?? lastSnapshot + index += 1 + yield* Console.log( + `snapshot ${snapshot.version} with ${snapshot.itemCount} items` + ) + return snapshot +}) + +const sameSnapshot = (left: Snapshot, right: Snapshot) => + left.version === right.version && left.itemCount === right.itemCount + +const initialState: StabilityState = { + previous: undefined, + current: undefined, + stable: false +} + +const untilStable = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.reduce( + () => initialState, + (state, current): StabilityState => ({ + previous: state.current, + current, + stable: state.current !== undefined && sameSnapshot(state.current, current) + }) + ), + Schedule.while(({ output }) => !output.stable) +) + +const program = Effect.gen(function*() { + const state = yield* readSnapshot.pipe(Effect.repeat(untilStable)) + yield* Console.log(`stable version: ${state.current?.version}`) +}) + +Effect.runPromise(program) +// Output: +// snapshot v1 with 10 items +// snapshot v2 with 12 items +// snapshot v2 with 12 items +// stable version: v2 +``` + +`readSnapshot` runs once before the schedule is consulted. The first successful +snapshot cannot be stable because there is no previous successful snapshot to +compare with. After each later success, the schedule compares the latest +snapshot with the previous one. When `sameSnapshot` returns `true`, the +`Schedule.while` predicate returns `false`, and the repeat stops. + +The returned value is the final `StabilityState`. Its `current` field is the +snapshot that matched `previous`. + +##### Variants + +Require several consecutive stable observations when a single match is not +strong enough. Carry a streak count in the reduced state and stop only after the +count reaches the required number of unchanged comparisons. + +##### Notes and caveats + +The stability predicate sees only successful outputs. Failures do not become +schedule inputs for `Effect.repeat`. + +Decide whether stability means "same as the immediately previous output" or +"within a tolerance". For numeric observations, exact equality is often the +wrong criterion; prefer an explicit tolerance such as an absolute delta. + +The first run is not delayed by the schedule. Delays apply only before later +recurrences. + +A stability schedule can run forever if the output never becomes stable. Add a +count limit, time budget, or external interruption when the surrounding workflow +does not already provide one. + +#### 11.5 Repeat until a terminal state is observed + +Use this when successful status observations should repeat until the observed +domain state is terminal. + +##### Problem + +A job observer, workflow monitor, or similar status check returns domain states +such as queued, running, succeeded, failed, or canceled. + +With `Effect.repeat`, the effect runs once before the schedule is consulted. +After each successful observation, the successful status value becomes the +schedule input. `Schedule.while` can allow another recurrence only while that +status is non-terminal. + +##### When to use it + +Use this when the repeated effect succeeds with a domain status even while the +domain workflow is still in progress. + +This is a good fit for small status-observation loops where states such as +`"queued"` and `"running"` mean "observe again", while states such as +`"succeeded"`, `"failed"`, or `"canceled"` mean "stop repeating". + +##### When not to use it + +Do not use this to retry failed observations. If the observation effect fails, +`Effect.repeat` stops with that failure before the schedule predicate can inspect +a status. + +Do not use this as a full polling recipe for external systems with deadlines, +logging, cancellation strategy, and failure classification. This recipe covers +only the repeat condition based on successful status observations. + +Do not leave the repeat unbounded unless the status is guaranteed to become +terminal or the fiber has a clear owner that can interrupt it. + +##### Schedule shape + +Make the successful status the schedule input, preserve it as the schedule +output, and continue while the latest status is not terminal. + +`Schedule.while` receives schedule metadata after a successful run. In +`Effect.repeat`, `metadata.input` is the successful output from the repeated +effect. Returning `true` allows another recurrence. Returning `false` stops the +repeat. + +The predicate above therefore repeats after successful non-terminal statuses and +stops as soon as a successful terminal status is observed. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type JobStatus = + | { readonly state: "queued" } + | { readonly state: "running"; readonly percent: number } + | { readonly state: "succeeded"; readonly resultId: string } + | { readonly state: "failed"; readonly reason: string } + | { readonly state: "canceled" } + +const isTerminal = (status: JobStatus): boolean => + status.state === "succeeded" || + status.state === "failed" || + status.state === "canceled" + +const statuses: ReadonlyArray = [ + { state: "queued" }, + { state: "running", percent: 40 }, + { state: "running", percent: 80 }, + { state: "succeeded", resultId: "result-123" } +] + +let index = 0 + +const observeJob = Effect.gen(function*() { + const lastStatus = statuses[statuses.length - 1]! + const status = statuses[index] ?? lastStatus + index += 1 + yield* Console.log(`observed ${status.state}`) + return status +}) + +const untilTerminal = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => !isTerminal(input)) +) + +const program = Effect.gen(function*() { + const terminalStatus = yield* observeJob.pipe( + Effect.repeat(untilTerminal) + ) + + yield* Console.log(`final state: ${terminalStatus.state}`) +}) + +Effect.runPromise(program) +// Output: +// observed queued +// observed running +// observed running +// observed succeeded +// final state: succeeded +``` + +`observeJob` runs once immediately. If it succeeds with a terminal status, there +are no recurrences. If it succeeds with a non-terminal status, the schedule +allows another observation. + +Because the schedule uses `Schedule.passthrough`, the repeated program succeeds +with the final successful `JobStatus` that made the predicate return `false`. + +##### Variants + +Add a pause and a recurrence cap when terminal status may take time but the loop +must still have a limit: + +Use the same terminal-status schedule, then compose it with +`Schedule.bothLeft(Schedule.recurs(20).pipe(Schedule.satisfiesInputType()))`. + +The repeat stops when either a successful terminal status is observed or the +recurrence cap is reached. `Schedule.recurs(20)` permits up to 20 recurrences +after the initial observation. + +##### Notes and caveats + +The terminal-state predicate inspects successful outputs only, after each +successful run. Failures from the observed effect do not become schedule inputs. + +The first observation is not delayed by the schedule. Spacing applies only +before later recurrences. + +Model terminal domain states as successful values when they are normal outcomes +of the observed workflow. Reserve the failure channel for failures of the +observation itself. + +When composing a count or timing schedule with `Schedule.while`, constrain the +input type with `Schedule.satisfiesInputType()` before reading +`metadata.input`. + +## Part IV — Polling Recipes + +### 12. Poll Until Completion + +#### 12.1 Poll a background job until done + +Use `Effect.repeat` with a spaced schedule when a submitted job exposes a +read-only status endpoint and should be observed until it reaches a terminal +domain state. + +##### Problem + +After submission returns a job id, a successful status check can still report +`"queued"` or `"running"`. Those are ordinary job states, not failures of the +status request. Polling should continue until a terminal state is observed. + +##### When to use it + +Use this when polling is driven by successful observations of a remote job's +state. + +This is a good fit for APIs that expose statuses such as `"queued"`, +`"running"`, `"succeeded"`, `"failed"`, or `"canceled"`, where the terminal +states are ordinary successful responses from the status endpoint. + +##### When not to use it + +Do not use this to retry a failing status endpoint. With `Effect.repeat`, a +failure from the status-check effect stops the repeat immediately. Use retry +around the status check when transport or decoding failures should be retried. + +Do not use this section as a timeout recipe. This recipe shows the basic polling +shape and a small recurrence cap. Deadline-oriented polling belongs in the +timeout recipes. + +Do not treat a domain `"failed"` job status as an effect failure unless your +caller explicitly wants job failure to fail the effect after polling completes. + +##### Schedule shape + +Use a timing schedule for the pause between status checks, constrain its input +to the status type, pass the latest status through as the schedule output, and +continue only while that status is not terminal. + +`Schedule.spaced("2 seconds")` supplies the delay before each recurrence. +`Schedule.satisfiesInputType()` constrains the timing schedule before +the predicate reads `metadata.input`. `Schedule.passthrough` keeps the successful +`JobStatus` as the schedule output, so the repeated effect returns the final +observed status. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type JobStatus = + | { readonly state: "queued" } + | { readonly state: "running"; readonly percent: number } + | { readonly state: "succeeded"; readonly resultId: string } + | { readonly state: "failed"; readonly reason: string } + | { readonly state: "canceled" } + +type StatusCheckError = { + readonly _tag: "StatusCheckError" + readonly message: string +} + +const isTerminal = (status: JobStatus): boolean => + status.state === "succeeded" || + status.state === "failed" || + status.state === "canceled" + +let step = 0 + +const nextStatus = (): JobStatus => { + step += 1 + switch (step) { + case 1: + return { state: "queued" } + case 2: + return { state: "running", percent: 40 } + default: + return { state: "succeeded", resultId: "result-123" } + } +} + +const checkJobStatus = (jobId: string): Effect.Effect => + Effect.gen(function*() { + const status = nextStatus() + yield* Console.log(`${jobId}: ${status.state}`) + return status + }) + +const pollUntilTerminal = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => !isTerminal(input)) +) + +const program = Effect.gen(function*() { + const finalStatus = yield* checkJobStatus("job-123").pipe( + Effect.repeat(pollUntilTerminal) + ) + yield* Console.log(`final status: ${finalStatus.state}`) +}) + +Effect.runPromise(program) +// Output: +// job-123: queued +// job-123: running +// job-123: succeeded +// final status: succeeded +``` + +The example checks immediately, logs two non-terminal statuses, waits briefly +between recurrences, and stops when `"succeeded"` is observed. + +The resulting effect succeeds with the terminal `JobStatus` when a terminal +status is observed. It fails with `StatusCheckError` only when a status check +effect fails. + +##### Variants + +Add a recurrence cap when the caller wants to stop after a small number of +observations even if the job is still non-terminal, for example by combining the +status schedule with `Schedule.recurs(30)` using `Schedule.bothLeft`. The result +is still a `JobStatus`: either terminal, or the last non-terminal status before +the cap stopped the repeat. + +If a terminal domain state should fail the caller, keep polling until the +terminal status is observed, then handle the final successful value in a +separate step. That keeps polling failures and job-domain failures distinct. + +##### Notes and caveats + +`Schedule.while` sees only successful outputs from the status check. It does not +classify effect failures. + +The first status check is not delayed. The schedule controls recurrences after +the first run. + +Use `Schedule.passthrough` when composing timing or counting schedules and the +caller needs the final observed status. + +When a timing or count schedule is combined with `Schedule.while`, apply +`Schedule.satisfiesInputType()` before reading `metadata.input`. + +#### 12.2 Poll payment status until settled + +Use polling when a payment provider reports in-flight and terminal states +through a read-only status endpoint. + +##### Problem + +The status request can succeed while the payment is still in flight, returning +domain states such as `"pending"` or `"processing"`. You want to poll successful +observations until the payment reaches a settled terminal state, such as +`"settled"`, `"failed"`, or `"canceled"`. + +##### When to use it + +Use this when polling is an observation loop: each request reads the current +payment status, and non-settled states mean "wait and observe again". + +This is a good fit when the payment provider clearly models in-progress and +terminal states, and those terminal states are normal business outcomes rather +than transport failures. + +##### When not to use it + +Do not use this to retry failed status requests. If the status effect fails, +`Effect.repeat` stops with that failure before the schedule can inspect a +payment status. + +Do not use this as the complete safety policy for payment writes. Creating, +capturing, refunding, or otherwise mutating a payment needs separate protection +around idempotency, duplicate submissions, and provider-specific guarantees. + +Do not leave production polling unbounded unless the fiber has an owner that can +interrupt it and the external system can tolerate the polling rate. + +##### Schedule shape + +Make the successful payment status the schedule input, preserve it as the +schedule output, and continue only while it is not settled. + +With `Effect.repeat`, the first status request runs immediately. After each +successful observation, the observed `PaymentStatus` becomes the schedule input. +`Schedule.while` returns `true` to allow another recurrence and `false` to stop. + +`Schedule.satisfiesInputType()` is applied before reading +`metadata.input`, because `Schedule.spaced` is a timing schedule and is not +constructed from `PaymentStatus` values. `Schedule.passthrough` keeps the final +observed status as the value returned by the repeated effect. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type PaymentStatus = + | { readonly state: "pending"; readonly paymentId: string } + | { readonly state: "processing"; readonly paymentId: string } + | { readonly state: "requires_review"; readonly paymentId: string } + | { readonly state: "settled"; readonly paymentId: string; readonly settlementId: string } + | { readonly state: "failed"; readonly paymentId: string; readonly reason: string } + | { readonly state: "canceled"; readonly paymentId: string } + +const isSettled = (status: PaymentStatus): boolean => + status.state === "settled" || + status.state === "failed" || + status.state === "canceled" + +let step = 0 + +const nextPaymentStatus = (): PaymentStatus => { + step += 1 + switch (step) { + case 1: + return { state: "pending", paymentId: "pay_123" } + case 2: + return { state: "processing", paymentId: "pay_123" } + default: + return { + state: "settled", + paymentId: "pay_123", + settlementId: "set_456" + } + } +} + +const observePaymentStatus = Effect.gen(function*() { + const status = nextPaymentStatus() + yield* Console.log(`payment ${status.paymentId}: ${status.state}`) + return status +}) + +const pollUntilSettled = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => !isSettled(input)) +) + +const program = Effect.gen(function*() { + const finalStatus = yield* observePaymentStatus.pipe( + Effect.repeat(pollUntilSettled) + ) + yield* Console.log(`final payment status: ${finalStatus.state}`) +}) + +Effect.runPromise(program) +// Output: +// payment pay_123: pending +// payment pay_123: processing +// payment pay_123: settled +// final payment status: settled +``` + +`observePaymentStatus` runs once before any delay. If the first successful +status is already settled, there are no recurrences. If the status is +`"pending"`, `"processing"`, or `"requires_review"`, the schedule waits two +seconds in production before observing again. The snippet uses a shorter delay +so it finishes quickly. + +The repeated effect succeeds with the terminal `PaymentStatus` that made +`isSettled` return `true`. + +##### Variants + +Use `Schedule.identity().pipe(Schedule.while(...))` only when +you want to demonstrate the stop condition without a delay. Real payment +polling should include spacing so successful non-terminal observations do not +turn into a tight loop. + +##### Notes and caveats + +Treat in-progress states as successful observations. `"pending"`, +`"processing"`, and similar states usually mean the provider accepted the status +request and the payment workflow is still moving. + +Treat terminal business states as successful observations too. A failed or +canceled payment can be the final answer from the payment domain, not a failure +of the status request itself. + +Keep the polling effect read-only. This recipe is about observing status until a +terminal state appears, not about repeating payment mutations. + +The first observation is not delayed by the schedule. Spacing applies only +before later recurrences. + +Choose a polling interval that is acceptable for the provider and for your +users. Time budgets, deadlines, and fallback behavior are separate recipes. + +#### 12.3 Poll an export job until ready + +Use polling when an export service returns an id before the exported file is +ready. + +##### Problem + +For a CSV or report export, the status request can succeed while the export is +still `"running"`. That is a domain state, not an effect failure. The effect +should fail only when the status request itself cannot be performed or decoded. + +##### When to use it + +Use this when an export API separates job creation from file readiness, and the +status endpoint returns ordinary business states such as `"running"`, `"ready"`, +or `"failed"`. + +This is a good fit when the caller wants the final observed export status and +can decide what to do with a ready download URL or a failed export reason. + +##### When not to use it + +Do not use this to retry failed status requests. With `Effect.repeat`, a failure +from the status-check effect stops the repeat immediately. If transport failures +should be retried, put retry behavior around the status check separately. + +Do not model an export-domain `"failed"` status as an effect failure inside the +polling schedule. Poll until the terminal domain state is observed, then decide +whether that final status should fail the caller. + +Do not use this as a timeout recipe. This section shows a polling loop with a +small recurrence cap. Deadline-oriented polling belongs in Chapter 17. + +##### Schedule shape + +Use a spaced schedule for the pause between status checks, preserve the latest +successful export status, and continue only while the export is still running. + +`Effect.repeat` runs the first status check immediately. After each successful +check, the resulting `ExportStatus` becomes the schedule input. +`Schedule.while` returns `true` for `"running"` so another check is scheduled, +and returns `false` for `"ready"` or `"failed"` so polling stops. + +`Schedule.satisfiesInputType()` is applied before reading +`metadata.input`, because `Schedule.spaced` is a timing schedule rather than a +schedule constructed from export statuses. `Schedule.passthrough` keeps the +latest `ExportStatus` as the value returned by the repeated effect. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type ExportStatus = + | { readonly state: "running"; readonly exportId: string; readonly percent: number } + | { readonly state: "ready"; readonly exportId: string; readonly downloadUrl: string } + | { readonly state: "failed"; readonly exportId: string; readonly reason: string } + +type ExportStatusError = { + readonly _tag: "ExportStatusError" + readonly message: string +} + +let step = 0 + +const nextExportStatus = (exportId: string): ExportStatus => { + step += 1 + switch (step) { + case 1: + return { state: "running", exportId, percent: 25 } + case 2: + return { state: "running", exportId, percent: 80 } + default: + return { + state: "ready", + exportId, + downloadUrl: "https://example.com/report.csv" + } + } +} + +const checkExportStatus = ( + exportId: string +): Effect.Effect => + Effect.gen(function*() { + const status = nextExportStatus(exportId) + yield* Console.log(`export ${exportId}: ${status.state}`) + return status + }) + +const pollUntilReadyOrFailed = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "running") +) + +const program = Effect.gen(function*() { + const finalStatus = yield* checkExportStatus("export-123").pipe( + Effect.repeat(pollUntilReadyOrFailed) + ) + yield* Console.log(`final export status: ${finalStatus.state}`) +}) + +Effect.runPromise(program) +// Output: +// export export-123: running +// export export-123: running +// export export-123: ready +// final export status: ready +``` + +The program succeeds with the first non-running status observed. That value may +be `"ready"` with a `downloadUrl`, or `"failed"` with a domain failure reason. + +It fails with `ExportStatusError` only when a status check effect fails. A +successful response whose state is `"failed"` is still a successful observation +from the status endpoint. + +##### Variants + +Add a recurrence cap when the caller wants to stop after a bounded number of +status checks even if the export is still running, for example by combining the +status schedule with `Schedule.recurs(40)` using `Schedule.bothLeft`. The final +value can be `"ready"`, `"failed"`, or the last `"running"` status if the cap +stops the repeat first. + +If the caller wants ready exports to succeed and failed exports to fail, keep +that decision after polling. The polling schedule should only decide whether to +observe again. + +##### Notes and caveats + +`Schedule.while` inspects successful status values. It does not see effect +failures from `checkExportStatus`. + +The first status request is not delayed. `Schedule.spaced("3 seconds")` controls +the delay before later recurrences. + +Keep export job creation outside this loop. This recipe repeats read-only status +checks, not the operation that starts the export. + +If a capped polling schedule returns a final `"running"` status, the export may +still complete later. Decide separately whether to surface that as "still +pending", enqueue a follow-up check, or escalate to a timeout policy from the +next chapter. + +#### 12.4 Poll cloud provisioning until ready + +Use polling when a cloud resource has been accepted for creation but is not +usable yet. + +##### Problem + +After a create request returns a resource id for a database, bucket, cluster, +VM, or service account, the provider exposes a read-only status endpoint. +Successful responses can say that provisioning is still `"pending"` or +`"creating"`, that the resource is `"ready"`, or that provisioning reached a +domain failure such as `"provisioning_failed"`. + +Those statuses are part of the cloud resource domain. They are not the same as +effect failures. The status-check effect should fail only when the status could +not be requested, authenticated, parsed, or decoded. + +##### When to use it + +Use this when the status endpoint returns ordinary domain states and the caller +needs the final observed provisioning state. + +This is a good fit for workflows where the resource id is already known, polling +is read-only, and `"ready"` and `"provisioning_failed"` are both terminal +answers from the provider. + +##### When not to use it + +Do not use this to repeat the create request. Provisioning APIs often require +idempotency keys or provider-specific conflict handling, and that belongs around +the submit step, not the polling loop. + +Do not use this to retry a failing status endpoint by itself. With +`Effect.repeat`, a failure from the status effect stops the repeat immediately. +If transport failures should be retried, apply retry policy inside the status +check or around it before the repeat. + +Do not turn a domain status like `"provisioning_failed"` into an effect failure +inside the polling schedule. Poll until the terminal status is observed, then +decide how the caller should handle that final successful value. + +##### Schedule shape + +Poll on a spaced schedule, preserve the latest successful status as the +schedule output, and continue only while the resource is still provisioning. + +`Schedule.spaced("5 seconds")` controls the delay before each recurrence. +`Schedule.satisfiesInputType()` constrains the timing +schedule before the predicate reads `metadata.input`. `Schedule.passthrough` +keeps the final observed status as the result of `Effect.repeat`. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type ProvisioningStatus = + | { readonly state: "pending"; readonly resourceId: string } + | { readonly state: "creating"; readonly resourceId: string } + | { readonly state: "configuring"; readonly resourceId: string } + | { readonly state: "ready"; readonly resourceId: string; readonly endpoint: string } + | { readonly state: "provisioning_failed"; readonly resourceId: string; readonly reason: string } + +type StatusCheckError = { + readonly _tag: "StatusCheckError" + readonly message: string +} + +const isProvisioning = (status: ProvisioningStatus): boolean => + status.state === "pending" || + status.state === "creating" || + status.state === "configuring" + +let step = 0 + +const nextProvisioningStatus = (resourceId: string): ProvisioningStatus => { + step += 1 + switch (step) { + case 1: + return { state: "pending", resourceId } + case 2: + return { state: "creating", resourceId } + default: + return { + state: "ready", + resourceId, + endpoint: "https://db.example.com" + } + } +} + +const describeResource = ( + resourceId: string +): Effect.Effect => + Effect.gen(function*() { + const status = nextProvisioningStatus(resourceId) + yield* Console.log(`resource ${resourceId}: ${status.state}`) + return status + }) + +const pollUntilReadyOrFailed = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => isProvisioning(input)) +) + +const program = Effect.gen(function*() { + const finalStatus = yield* describeResource("db-123").pipe( + Effect.repeat(pollUntilReadyOrFailed) + ) + yield* Console.log(`final provisioning status: ${finalStatus.state}`) +}) + +Effect.runPromise(program) +// Output: +// resource db-123: pending +// resource db-123: creating +// resource db-123: ready +// final provisioning status: ready +``` + +The program performs the first status check immediately. If the first +successful response is `"ready"` or `"provisioning_failed"`, the schedule stops +without another request. If the resource is still provisioning, the schedule +waits before checking again. + +The returned effect succeeds with the final `ProvisioningStatus`. It fails with +`StatusCheckError` only when `describeResource` fails. + +##### Variants + +Add a recurrence cap when the caller wants to stop after a bounded number of +successful observations, for example by combining the status schedule with +`Schedule.recurs(40)` using `Schedule.bothLeft`. The returned value may be +terminal, or it may be the last non-terminal status observed when the cap +stopped the repeat. + +After polling, map the final successful status into the shape your application +needs. For example, a caller may return the ready endpoint, surface a +provisioning failure as a domain error, or store the last non-terminal status +for an operator to inspect. + +##### Notes and caveats + +`Schedule.while` sees successful status values. It does not inspect or recover +effect failures from the status request. + +Keep provisioning statuses distinct from request failures. `"ready"` and +`"provisioning_failed"` are terminal domain states; `StatusCheckError` means the +program could not observe the state. + +The first status check is not delayed. The schedule controls only recurrences +after the first successful check. + +Choose an interval that respects the provider's rate limits and expected +provisioning latency. Deadlines, startup budgets, and fallback behavior are +separate recipes. + +#### 12.5 Poll until status becomes `Completed` + +Polling for a desired output is not the same as polling until any terminal state +appears. The schedule decides when to ask again; the code after polling decides +whether the final status is the desired one. + +##### Problem + +A status endpoint may successfully return `"Queued"`, `"Running"`, +`"Completed"`, `"Failed"`, or `"Canceled"`. Only `"Completed"` is the result +the caller wants. + +`"Failed"` and `"Canceled"` are terminal domain states: successful status +responses that mean the job will not complete. They should stop polling, but +they should not be treated as completed work. A failed status request is a +separate effect failure. + +##### When to use it + +Use this for job APIs where in-progress statuses mean "poll again", one status +means "return the completed result", and other terminal statuses must be +reported separately. + +##### When not to use it + +Do not retry transport, authorization, or decoding failures with this schedule. +With `Effect.repeat`, failures from the repeated effect stop the repeat unless +handled before repeating. + +Do not continue while `status.state !== "Completed"` when the domain has other +terminal states. That would keep polling after a job has already failed or been +canceled. + +Do not leave long-running jobs unbounded unless another owner controls the +fiber lifetime. + +##### Schedule shape + +Use `Schedule.spaced` for the delay, `Schedule.passthrough` to keep the latest +status, and `Schedule.while` to continue only while the status is still in +progress. After `Effect.repeat` returns, map `"Completed"` to success and map +other terminal statuses to domain errors. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type JobStatus = + | { readonly state: "Queued" } + | { readonly state: "Running"; readonly percent: number } + | { readonly state: "Completed"; readonly resultId: string } + | { readonly state: "Failed"; readonly reason: string } + | { readonly state: "Canceled" } + +type CompletedStatus = Extract + +type CompletionError = + | { readonly _tag: "JobFailed"; readonly reason: string } + | { readonly _tag: "JobCanceled" } + | { readonly _tag: "JobDidNotCompleteInTime"; readonly lastState: JobStatus["state"] } + +const scriptedStatuses: ReadonlyArray = [ + { state: "Queued" }, + { state: "Running", percent: 40 }, + { state: "Completed", resultId: "result-123" } +] + +let readIndex = 0 + +const isInProgress = (status: JobStatus): boolean => status.state === "Queued" || status.state === "Running" + +const checkJobStatus = (jobId: string): Effect.Effect => + Effect.sync(() => { + const status = scriptedStatuses[ + Math.min(readIndex, scriptedStatuses.length - 1) + ]! + readIndex += 1 + return status + }).pipe( + Effect.tap((status) => Console.log(`[${jobId}] ${status.state}`)) + ) + +const pollWhileInProgress = Schedule.spaced("20 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => isInProgress(input)), + Schedule.take(10) +) + +const requireCompleted = ( + status: JobStatus +): Effect.Effect => { + switch (status.state) { + case "Completed": + return Effect.succeed(status) + case "Failed": + return Effect.fail({ _tag: "JobFailed", reason: status.reason }) + case "Canceled": + return Effect.fail({ _tag: "JobCanceled" }) + case "Queued": + case "Running": + return Effect.fail({ + _tag: "JobDidNotCompleteInTime", + lastState: status.state + }) + } +} + +const program = checkJobStatus("job-1").pipe( + Effect.repeat(pollWhileInProgress), + Effect.flatMap(requireCompleted), + Effect.tap((status) => Console.log(`completed with ${status.resultId}`)) +) + +Effect.runPromise(program).then((status) => { + console.log("result:", status) +}) +// Output: +// [job-1] Queued +// [job-1] Running +// [job-1] Completed +// completed with result-123 +// result: { state: 'Completed', resultId: 'result-123' } +``` + +The first check runs immediately. The schedule repeats only while the latest +successful status is `"Queued"` or `"Running"`. The final interpretation is +kept outside the schedule so `"Failed"` and `"Canceled"` remain visible domain +outcomes. + +##### Variants + +Remove `Schedule.take` when another lifetime or timeout bounds the polling +fiber. Keep an explicit branch for in-progress statuses if you add any schedule +that can stop before completion. + +If failed or canceled jobs should be returned as values instead of failures, +keep the same polling schedule and change only the final interpreter. + +##### Notes and caveats + +`Schedule.while` sees successful status values only. It does not inspect +failures from the status-check effect. + +The first status check is not delayed. Delays apply only before recurrences. + +Use `Schedule.satisfiesInputType()` before `Schedule.while` when a timing +schedule reads the latest successful status from `metadata.input`. + +### 13. Poll for Resource State + +#### 13.1 Poll until a resource exists + +Model "not found yet" as a successful observation when absence is expected to +be temporary. The schedule can then repeat on that observation without +confusing it with transport or decoding failure. + +##### Problem + +A lookup for a newly created object, uploaded file, provisioned endpoint, or +generated artifact may succeed and report either "missing" or "found." The loop +should wait between missing observations and stop as soon as the resource is +found. + +Keep real lookup failures separate. Authorization errors, malformed responses, +network failures, and invalid identifiers should not be silently turned into +"missing" unless the domain explicitly says so. + +##### When to use it + +Use this when absence is a normal temporary state and the caller wants to wait +until the resource becomes visible. + +This fits APIs where a lookup, `HEAD` request, or metadata read can distinguish +"missing for now" from an actual failed request. + +##### When not to use it + +Do not use this for rich status workflows with states such as `"Queued"`, +`"Running"`, `"Failed"`, and `"Completed"`. Poll the status model and handle +each terminal state instead. + +Do not leave the poll unbounded when the resource may never appear. Add a cap, +time budget, or owning fiber lifetime. + +##### Schedule shape + +Use `Schedule.spaced` for the delay, `Schedule.passthrough` to return the latest +lookup result, and `Schedule.while` to continue only while the lookup is +`Missing`. + +If a bounded schedule stops first, the final observation can still be +`Missing`; interpret that case explicitly. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +interface Resource { + readonly id: string + readonly url: string +} + +type ResourceLookup = + | { readonly _tag: "Missing" } + | { readonly _tag: "Found"; readonly resource: Resource } + +type WaitForResourceError = { + readonly _tag: "ResourceNotFoundInTime" + readonly resourceId: string +} + +const scriptedLookups: ReadonlyArray = [ + { _tag: "Missing" }, + { _tag: "Missing" }, + { _tag: "Found", resource: { id: "file-1", url: "https://example.test/file-1" } } +] + +let readIndex = 0 + +const lookupResource = (resourceId: string): Effect.Effect => + Effect.sync(() => { + const lookup = scriptedLookups[ + Math.min(readIndex, scriptedLookups.length - 1) + ]! + readIndex += 1 + return lookup + }).pipe( + Effect.tap((lookup) => Console.log(`[${resourceId}] ${lookup._tag}`)) + ) + +const pollUntilFound = Schedule.spaced("15 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input._tag === "Missing"), + Schedule.take(10) +) + +const requireFound = ( + resourceId: string, + lookup: ResourceLookup +): Effect.Effect => + lookup._tag === "Found" + ? Effect.succeed(lookup.resource) + : Effect.fail({ _tag: "ResourceNotFoundInTime", resourceId }) + +const program = lookupResource("file-1").pipe( + Effect.repeat(pollUntilFound), + Effect.flatMap((lookup) => requireFound("file-1", lookup)), + Effect.tap((resource) => Console.log(`resource url: ${resource.url}`)) +) + +Effect.runPromise(program).then((resource) => { + console.log("result:", resource) +}) +// Output: +// [file-1] Missing +// [file-1] Missing +// [file-1] Found +// resource url: https://example.test/file-1 +// result: { id: 'file-1', url: 'https://example.test/file-1' } +``` + +The first lookup runs immediately. Missing observations wait before the next +lookup. A found observation stops the repeat and is returned as the resource. + +##### Variants + +Add `Schedule.jittered` when many callers may wait for the same dependency and +aligned lookups would be noisy. + +If the underlying API reports a temporary 404 as an error, translate only that +specific case into `Missing` before `Effect.repeat`. Leave unrelated failures in +the effect error channel or retry them with a separate retry policy. + +##### Notes and caveats + +`Effect.repeat` repeats after success. A failed lookup stops the repeat unless +the lookup effect handles that failure first. + +The first lookup is not delayed by the schedule. + +Model only genuinely temporary absence as `Missing`. A permanently invalid id +or an unauthorized caller should fail or return a separate domain result. + +#### 13.2 Poll until a cache entry appears + +A cache miss can be an ordinary successful observation. When another process is +expected to populate the value soon, repeat on misses and stop on the first +present entry. + +##### Problem + +A background warm-up fiber, write-through path, or external producer may fill a +cache after the caller starts looking. The polling loop should wait between +cache reads, stop at the first present entry, and keep cache backend failures +separate from normal misses. + +##### When to use it + +Use this when a missing cache entry is expected to be temporary and the caller +wants a short wait for population. + +This fits asynchronous warm-up, write-through propagation to an in-process +cache, or a shared cache that another worker fills after a known trigger. + +##### When not to use it + +Do not use this as a general resource-creation workflow. The recipe assumes a +cache population path is already in motion. + +Do not treat cache backend failures as misses. Network errors, serialization +errors, permission errors, and unavailable cache servers should remain failures +unless the domain deliberately models them as successful misses. + +Do not poll indefinitely for keys that may never be written. + +##### Schedule shape + +Use `Schedule.spaced` for a small delay between cache reads, +`Schedule.passthrough` to keep the latest lookup, and `Schedule.while` to repeat +only while the lookup is `Missing`. + +For bounded waits, handle a final `Missing` value explicitly. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +interface CacheEntry { + readonly key: string + readonly value: string + readonly version: number +} + +type CacheLookup = + | { readonly _tag: "Missing" } + | { readonly _tag: "Present"; readonly entry: CacheEntry } + +type WaitForCacheEntryError = { + readonly _tag: "CacheEntryUnavailable" + readonly key: string +} + +const scriptedLookups: ReadonlyArray = [ + { _tag: "Missing" }, + { _tag: "Missing" }, + { _tag: "Present", entry: { key: "user:1", value: "Ada", version: 3 } } +] + +let readIndex = 0 + +const lookupCacheEntry = (key: string): Effect.Effect => + Effect.sync(() => { + const lookup = scriptedLookups[ + Math.min(readIndex, scriptedLookups.length - 1) + ]! + readIndex += 1 + return lookup + }).pipe( + Effect.tap((lookup) => Console.log(`[${key}] ${lookup._tag}`)) + ) + +const pollUntilPresent = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input._tag === "Missing"), + Schedule.take(10) +) + +const requirePresent = ( + key: string, + lookup: CacheLookup +): Effect.Effect => + lookup._tag === "Present" + ? Effect.succeed(lookup.entry) + : Effect.fail({ _tag: "CacheEntryUnavailable", key }) + +const program = lookupCacheEntry("user:1").pipe( + Effect.repeat(pollUntilPresent), + Effect.flatMap((lookup) => requirePresent("user:1", lookup)), + Effect.tap((entry) => Console.log(`cache value: ${entry.value} v${entry.version}`)) +) + +Effect.runPromise(program).then((entry) => { + console.log("result:", entry) +}) +// Output: +// [user:1] Missing +// [user:1] Missing +// [user:1] Present +// cache value: Ada v3 +// result: { key: 'user:1', value: 'Ada', version: 3 } +``` + +The first cache lookup runs immediately. Misses wait before the next read. The +first present entry stops the repeat and becomes the result. + +##### Variants + +Add `Schedule.jittered` when many callers may wait for the same key and aligned +reads would create avoidable cache traffic. + +Use a small recurrence cap for user-facing waits. A cache should not become a +hidden unbounded dependency in the request path. + +If a cache API represents a miss as an error, recover only that miss into +`Missing` before repeating. Keep backend failures as failures. + +##### Notes and caveats + +`Schedule.while` sees successful lookup results only. + +`Effect.repeat` repeats after success. A failed cache read stops the repeat +unless handled before repeating. + +A miss should be temporary for this recipe. If no population path is active, +return a separate domain result or fail instead of polling as if the entry will +appear. + +#### 13.3 Poll until replication catches up + +Replication-aware polling should ask a narrow question: has the lagging view +observed at least the version the caller already knows exists? + +##### Problem + +After writing to a primary system, the caller may receive a version, watermark, +or cursor. A follower, read model, replica, or search index can lag behind that +position for a short time. + +Poll the replicated view until its observed position reaches the required +position. Treat "behind" as a successful observation, not as a failed read. + +##### When to use it + +Use this when the caller has a concrete target position and the downstream view +can report a comparable observed position. + +This fits event stream versions, projection watermarks, indexing sequence +numbers, and read models that expose the cursor they have processed. + +##### When not to use it + +Do not use this when the follower cannot report a comparable position. Polling +for "maybe visible now" is a different shape. + +Do not hide failed replica reads. Timeouts, authorization errors, decode +failures, and unavailable read models should remain effect failures unless your +domain explicitly recovers them. + +Do not compare opaque cursor strings lexicographically unless the producer +defines that order. + +##### Schedule shape + +Use `Schedule.spaced` for the read interval, `Schedule.passthrough` to keep the +latest observation, and `Schedule.while` to continue only while the observed +version is below the required version. + +If you add a bound, handle the final behind observation as "did not catch up in +time" instead of returning stale data. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +interface ReplicaObservation { + readonly replica: "read-model" + readonly observedVersion: number +} + +type WaitForReplicaError = { + readonly _tag: "ReplicaDidNotCatchUp" + readonly requiredVersion: number + readonly observedVersion: number +} + +const scriptedObservations: ReadonlyArray = [ + { replica: "read-model", observedVersion: 41 }, + { replica: "read-model", observedVersion: 43 }, + { replica: "read-model", observedVersion: 45 } +] + +let readIndex = 0 + +const hasCaughtUp = ( + observation: ReplicaObservation, + requiredVersion: number +): boolean => observation.observedVersion >= requiredVersion + +const readReplicaWatermark = ( + streamName: string +): Effect.Effect => + Effect.sync(() => { + const observation = scriptedObservations[ + Math.min(readIndex, scriptedObservations.length - 1) + ]! + readIndex += 1 + return observation + }).pipe( + Effect.tap((observation) => + Console.log( + `[${streamName}] ${observation.replica} at ${observation.observedVersion}` + ) + ) + ) + +const pollUntilVersion = (requiredVersion: number) => + Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => !hasCaughtUp(input, requiredVersion)), + Schedule.take(10) + ) + +const requireCaughtUp = ( + requiredVersion: number, + observation: ReplicaObservation +): Effect.Effect => + hasCaughtUp(observation, requiredVersion) + ? Effect.succeed(observation) + : Effect.fail({ + _tag: "ReplicaDidNotCatchUp", + requiredVersion, + observedVersion: observation.observedVersion + }) + +const requiredVersion = 45 + +const program = readReplicaWatermark("orders").pipe( + Effect.repeat(pollUntilVersion(requiredVersion)), + Effect.flatMap((observation) => requireCaughtUp(requiredVersion, observation)), + Effect.tap((observation) => Console.log(`caught up at ${observation.observedVersion}`)) +) + +Effect.runPromise(program).then((observation) => { + console.log("result:", observation) +}) +// Output: +// [orders] read-model at 41 +// [orders] read-model at 43 +// [orders] read-model at 45 +// caught up at 45 +// result: { replica: 'read-model', observedVersion: 45 } +``` + +The first read runs immediately. Behind observations wait before the next read. +Once the replica reports the required version or later, the schedule stops. + +##### Variants + +Add `Schedule.jittered` when many clients may wait on the same replica and +aligned read bursts would add load. + +For opaque cursors, keep the same schedule shape but replace the numeric +comparison with a domain comparison that knows whether the observed cursor has +reached the required cursor. + +Use a target position from the write path or another authoritative source. A +guessed target can make polling report success for the wrong point in history. + +##### Notes and caveats + +`Schedule.while` sees successful replica observations only. It does not inspect +read failures. + +`Effect.repeat` repeats successes. Retry transient failed reads separately when +that is appropriate. + +The first read is not delayed by the schedule. + +#### 13.4 Poll until eventual consistency settles + +Eventually consistent reads can succeed while still showing an old view. Treat +those stale reads as observations and poll until a concrete condition says the +view has caught up. + +##### Problem + +After a write, the caller may know the expected revision, version, cursor, or +checksum. The read side may lag for a short time, so the first few reads can be +valid but stale. + +The polling loop should stop when the expected state is visible, keep stale +observations separate from read failures, and avoid polling forever after the +view has advanced far enough to prove the expected data is absent. + +##### When to use it + +Use this when stale reads are normal temporary observations and the caller has a +specific condition for "settled." + +This fits command acceptance followed by asynchronous projection updates, event +publication followed by a read model, or search indexing that exposes enough +state to verify the write has appeared. + +##### When not to use it + +Do not use polling to claim strict read-after-write consistency. It can wait for +an eventually consistent view; it does not make the dependency strongly +consistent. + +Do not turn read failures into "not settled yet" unless the domain deliberately +models them that way. + +Do not keep polling after the view revision has passed the expected revision but +the expected record is still missing. That is a domain inconsistency, not a +stale read. + +##### Schedule shape + +Represent each successful read as `Behind`, `Settled`, or `Inconsistent`. Use +`Schedule.spaced`, `Schedule.passthrough`, and `Schedule.while` to repeat only +while the latest observation is `Behind`. + +After polling, interpret `Settled` as success, `Inconsistent` as a domain +failure, and a bounded final `Behind` as "not settled in time." + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +interface OrderSummary { + readonly orderId: string + readonly revision: number + readonly totalCents: number +} + +interface AccountOrdersView { + readonly accountId: string + readonly revision: number + readonly orders: ReadonlyArray +} + +type ProjectionObservation = + | { + readonly _tag: "Behind" + readonly view: AccountOrdersView + readonly expectedRevision: number + } + | { + readonly _tag: "Settled" + readonly view: AccountOrdersView + readonly order: OrderSummary + } + | { + readonly _tag: "Inconsistent" + readonly view: AccountOrdersView + readonly reason: string + } + +type ProjectionWaitError = + | { + readonly _tag: "ProjectionDidNotSettleInTime" + readonly expectedRevision: number + readonly observedRevision: number + } + | { + readonly _tag: "ProjectionDidNotContainExpectedOrder" + readonly reason: string + } + +const scriptedViews: ReadonlyArray = [ + { accountId: "account-1", revision: 8, orders: [] }, + { accountId: "account-1", revision: 9, orders: [] }, + { + accountId: "account-1", + revision: 10, + orders: [{ orderId: "order-7", revision: 10, totalCents: 2599 }] + } +] + +let readIndex = 0 + +const findOrder = ( + view: AccountOrdersView, + orderId: string +): OrderSummary | undefined => view.orders.find((order) => order.orderId === orderId) + +const readAccountOrders = ( + accountId: string +): Effect.Effect => + Effect.sync(() => { + const view = scriptedViews[ + Math.min(readIndex, scriptedViews.length - 1) + ]! + readIndex += 1 + return view + }).pipe( + Effect.tap((view) => Console.log(`[${accountId}] read revision ${view.revision}`)) + ) + +const observeAccountOrders = ( + accountId: string, + expectedRevision: number, + orderId: string +): Effect.Effect => + readAccountOrders(accountId).pipe( + Effect.map((view): ProjectionObservation => { + const order = findOrder(view, orderId) + + if (order !== undefined && view.revision >= expectedRevision) { + return { _tag: "Settled", view, order } + } + + if (view.revision < expectedRevision) { + return { _tag: "Behind", view, expectedRevision } + } + + return { + _tag: "Inconsistent", + view, + reason: "Projection reached the expected revision without the order" + } + }), + Effect.tap((observation) => Console.log(`observation: ${observation._tag}`)) + ) + +const pollUntilProjectionSettles = Schedule.spaced("15 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input._tag === "Behind"), + Schedule.take(10) +) + +const requireSettled = ( + expectedRevision: number, + observation: ProjectionObservation +): Effect.Effect => { + switch (observation._tag) { + case "Settled": + return Effect.succeed(observation.order) + case "Inconsistent": + return Effect.fail({ + _tag: "ProjectionDidNotContainExpectedOrder", + reason: observation.reason + }) + case "Behind": + return Effect.fail({ + _tag: "ProjectionDidNotSettleInTime", + expectedRevision, + observedRevision: observation.view.revision + }) + } +} + +const expectedRevision = 10 + +const program = observeAccountOrders( + "account-1", + expectedRevision, + "order-7" +).pipe( + Effect.repeat(pollUntilProjectionSettles), + Effect.flatMap((observation) => requireSettled(expectedRevision, observation)), + Effect.tap((order) => Console.log(`settled order total: ${order.totalCents}`)) +) + +Effect.runPromise(program).then((order) => { + console.log("result:", order) +}) +// Output: +// [account-1] read revision 8 +// observation: Behind +// [account-1] read revision 9 +// observation: Behind +// [account-1] read revision 10 +// observation: Settled +// settled order total: 2599 +// result: { orderId: 'order-7', revision: 10, totalCents: 2599 } +``` + +The first read runs immediately. While the projection is behind the expected +revision, later reads wait for the schedule delay. Once the expected order is +visible at the expected revision or later, polling stops. + +##### Variants + +Add `Schedule.jittered` when many callers may poll the same projection and +aligned reads would add load. + +If you do not have an expected revision, use a stricter stability signal such +as the same projection version or checksum appearing in consecutive reads. That +is weaker than checking a known target, so keep the wait bounded. + +If the view advances beyond the expected revision without the expected data, +return a domain inconsistency instead of continuing to poll. + +##### Notes and caveats + +`Schedule.while` sees successful observations only. It does not inspect read +failures. + +`Effect.repeat` repeats successes. Retry transient failed reads separately when +that is appropriate. + +Prefer a concrete expected revision, version, or checksum over vague "looks +settled" checks. The schedule should not encode replication internals. + +### 14. Poll with Timeouts + +#### 14.1 Poll every second for up to 30 seconds + +Use this for a short status poll: run the check once immediately, then keep +checking roughly once per second while the last successful status is still +pending. The schedule controls recurrence; ordinary Effect code interprets the +final status. + +##### Problem + +The status endpoint can succeed with `"pending"`, `"ready"`, or `"failed"`. +Only `"pending"` should request another poll, and polling should stop once the +30-second recurrence budget is exhausted. + +The budget is not a hard timeout for a request already in flight. It is checked +between successful status observations. + +##### When to use it + +Use it for readiness checks, job-status endpoints, and eventually consistent +projections where "not ready yet" is a successful domain value. + +##### When not to use it + +Do not use it to retry failed status requests. `Effect.repeat` stops when the +checked effect fails. + +Do not rely on `Schedule.during("30 seconds")` to interrupt slow requests. Use +`Effect.timeout` on the status check, or around the whole workflow, when the +caller needs interruption semantics. + +##### Schedule shape + +Use `Schedule.spaced("1 second")` for the cadence, `Schedule.while` for the +pending-status predicate, and `Schedule.during("30 seconds")` for the elapsed +recurrence budget. + +Beginner note: Schedule output — `Schedule.passthrough` is intentional here: +the repeat result is the last status observed by the schedule. + +##### Example + +```ts +import { Clock, Effect, Fiber, Schedule } from "effect" +import { TestClock } from "effect/testing" + +type Status = + | { readonly state: "pending" } + | { readonly state: "ready"; readonly resourceId: string } + | { readonly state: "failed"; readonly reason: string } + +const script: ReadonlyArray = [ + { state: "pending" }, + { state: "pending" }, + { state: "ready", resourceId: "resource-123" } +] + +const pollEverySecondForUpTo30Seconds = Schedule.spaced("1 second").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "pending"), + Schedule.bothLeft( + Schedule.during("30 seconds").pipe( + Schedule.satisfiesInputType() + ) + ) +) + +let checks = 0 + +const checkStatus = Effect.gen(function*() { + const now = yield* Clock.currentTimeMillis + const status = script[Math.min(checks, script.length - 1)]! + checks += 1 + console.log(`t+${now}ms check ${checks}: ${status.state}`) + return status +}) + +const program = Effect.gen(function*() { + const fiber = yield* checkStatus.pipe( + Effect.repeat(pollEverySecondForUpTo30Seconds), + Effect.forkDetach + ) + + yield* TestClock.adjust("30 seconds") + + const finalStatus = yield* Fiber.join(fiber) + console.log("final:", finalStatus) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program) +``` + +The example uses `TestClock` so it can run in `scratchpad/repro.ts` without +waiting for real seconds. The policy itself still uses a one-second interval and +a 30-second recurrence budget. + +##### Variants + +Apply `Effect.timeout("2 seconds")` to `checkStatus` when each individual +request needs its own deadline. That timeout can interrupt the request; the +schedule still only decides whether to poll again after a successful response. + +Use `Schedule.fixed("1 second")` instead of `Schedule.spaced("1 second")` when +polls should target wall-clock boundaries rather than waiting one second after +each completed check. + +##### Notes and caveats + +The first check is immediate. `Schedule.during` is approximate for the whole +workflow because it is consulted between successful checks. `Schedule.while` +sees successful status values only, so transport and decoding failures remain in +the effect failure channel. + +#### 14.2 Give up when the operation is clearly too slow + +Use this when continuing to poll is no longer useful after a practical elapsed +budget. The operation may still finish later, but this caller should stop +waiting and make that outcome explicit. + +##### Problem + +A status check can keep succeeding with `"pending"`. The schedule should stop +polling after a budget even when no terminal status has appeared, and it should +still stop earlier for `"ready"` or `"failed"`. + +##### When to use it + +Use it when slowness is a domain or operational outcome, not a transport +failure. This fits user-facing waits, orchestration steps, readiness checks, and +integrations where continued polling would waste capacity. + +##### When not to use it + +Do not use it as a hard interruption timeout. `Schedule.during` is evaluated +between successful status checks; it does not cancel a status check already in +flight. + +Do not collapse a domain `"failed"` status and a slow `"pending"` status into +the same case unless the caller truly handles them the same way. They usually +mean different things operationally. + +##### Schedule shape + +Use a spaced cadence, preserve the latest successful status with +`Schedule.passthrough`, continue only while that status is `"pending"`, and +combine the policy with `Schedule.during` to cap the recurrence window. + +##### Example + +```ts +import { Clock, Effect, Fiber, Schedule } from "effect" +import { TestClock } from "effect/testing" + +type OperationStatus = + | { readonly state: "pending"; readonly operationId: string } + | { readonly state: "ready"; readonly operationId: string; readonly resourceId: string } + | { readonly state: "failed"; readonly operationId: string; readonly reason: string } + +const giveUpWhenTooSlow = Schedule.spaced("2 seconds").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "pending"), + Schedule.bothLeft( + Schedule.during("8 seconds").pipe( + Schedule.satisfiesInputType() + ) + ) +) + +let checks = 0 + +const checkOperationStatus = Effect.gen(function*() { + const now = yield* Clock.currentTimeMillis + checks += 1 + + const status: OperationStatus = { + state: "pending", + operationId: "operation-1" + } + + console.log(`t+${now}ms check ${checks}: ${status.state}`) + return status +}) + +const program = Effect.gen(function*() { + const fiber = yield* checkOperationStatus.pipe( + Effect.repeat(giveUpWhenTooSlow), + Effect.forkDetach + ) + + yield* TestClock.adjust("12 seconds") + + const finalStatus = yield* Fiber.join(fiber) + console.log("final:", finalStatus) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program) +``` + +The final status is still `"pending"`, which is the signal that the schedule +stopped because the operation was too slow for this caller. + +##### Variants + +If each check needs its own deadline, apply `Effect.timeout("3 seconds")` to the +checked effect. That can interrupt an in-flight check; the schedule cannot. + +If the caller needs a typed timeout error, inspect the final status after +`Effect.repeat` and map final `"pending"` to a domain error. Section 17.5 shows +that shape. + +Use `Schedule.fixed("2 seconds")` instead of `Schedule.spaced("2 seconds")` +when the polling loop should target fixed wall-clock boundaries rather than +waiting two seconds after each successful check completes. + +##### Notes and caveats + +The first check is immediate. The duration budget is approximate for the whole +workflow because it is checked between successful runs. Failed status-check +effects do not become schedule inputs. + +#### 14.3 Distinguish “still running” from “failed permanently” + +Use this when a status endpoint reports several successful domain states, but +only some of them mean work is still in progress. The schedule should continue +for in-progress states and stop for terminal states, including domain failures. + +##### Problem + +`"queued"` and `"running"` should poll again. `"succeeded"`, `"failed"`, and +`"canceled"` should stop. A status value of `"failed"` is different from a +failed status request: the request succeeded and reported a terminal domain +outcome. + +##### Why this comparison matters + +`Effect.repeat` repeats after successful effects. With polling, the status +check can succeed even when the remote job reports permanent failure. That +successful status becomes the schedule input, so the repeat predicate must be +about domain state, not request success. + +If `"failed"` is treated like `"running"`, the caller keeps polling a job that +is already finished. If `"running"` is treated like an error, the caller stops +before the workflow has had a chance to complete. + +Keep the repeat predicate narrow: continue only for statuses that are truly +in progress. After the repeat stops, interpret the final observed status. + +##### Schedule shape + +Classify in-progress statuses with a predicate such as `isStillRunning`, use it +from `Schedule.while`, and keep the final `JobStatus` with +`Schedule.passthrough`. + +##### Example + +```ts +import { Clock, Effect, Fiber, Schedule } from "effect" +import { TestClock } from "effect/testing" + +type JobStatus = + | { readonly state: "queued"; readonly jobId: string } + | { readonly state: "running"; readonly jobId: string; readonly progress: number } + | { readonly state: "succeeded"; readonly jobId: string; readonly resultId: string } + | { readonly state: "failed"; readonly jobId: string; readonly reason: string } + | { readonly state: "canceled"; readonly jobId: string } + +const isStillRunning = (status: JobStatus): boolean => status.state === "queued" || status.state === "running" + +const pollWhileStillRunning = Schedule.spaced("2 seconds").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => isStillRunning(input)), + Schedule.bothLeft( + Schedule.during("1 minute").pipe(Schedule.satisfiesInputType()) + ) +) + +type PollResult = + | { readonly _tag: "Completed"; readonly resultId: string } + | { readonly _tag: "FailedPermanently"; readonly reason: string } + | { readonly _tag: "Canceled" } + | { readonly _tag: "StillRunning"; readonly status: Extract } + +const interpretFinalStatus = (status: JobStatus): PollResult => { + switch (status.state) { + case "succeeded": + return { _tag: "Completed", resultId: status.resultId } + case "failed": + return { _tag: "FailedPermanently", reason: status.reason } + case "canceled": + return { _tag: "Canceled" } + case "queued": + case "running": + return { _tag: "StillRunning", status } + } +} + +const script: ReadonlyArray = [ + { state: "queued", jobId: "job-1" }, + { state: "running", jobId: "job-1", progress: 40 }, + { state: "failed", jobId: "job-1", reason: "validation failed" } +] + +let checks = 0 + +const checkJobStatus = Effect.gen(function*() { + const now = yield* Clock.currentTimeMillis + const status = script[Math.min(checks, script.length - 1)]! + checks += 1 + console.log(`t+${now}ms check ${checks}: ${status.state}`) + return status +}) + +const program = Effect.gen(function*() { + const fiber = yield* checkJobStatus.pipe( + Effect.repeat(pollWhileStillRunning), + Effect.map(interpretFinalStatus), + Effect.forkDetach + ) + + yield* TestClock.adjust("1 minute") + + const result = yield* Fiber.join(fiber) + console.log("result:", result) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program) +``` + +The final `"failed"` status stops polling because it is not still running. The +interpreter then maps it to a domain result. + +##### Tradeoffs + +Keeping terminal domain failures as successful statuses makes the repeat logic +clear: the schedule stops because the status is no longer in progress. The +caller can then decide whether `"failed"` should become a typed failure, a +return value, a log entry, or a user-facing message. + +Mapping permanent domain failures into the effect failure channel before +`Effect.repeat` can be useful when the rest of the program already models them +as failures. The cost is that the schedule no longer sees those statuses. The +repeat stops because the effect failed, not because `Schedule.while` classified +the status as terminal. + +For polling APIs, successful status values usually drive the schedule, while +transport, authorization, and decoding problems stay in the effect failure +channel. + +##### Recommended default + +Model ordinary workflow states as successful values. Use a predicate such as +`isStillRunning` for `Schedule.while`, and make that predicate return `true` +only for states that should cause another poll. + +After `Effect.repeat` returns, interpret the final observed status. Treat a +permanent failed terminal status as a domain outcome at that boundary, not as a +reason to keep polling. + +##### Notes and caveats + +`Schedule.while` sees successful status values only. A schedule-side duration +limits recurrences but does not interpret the final status or interrupt an +in-flight status check. Keep the in-progress predicate explicit; a catch-all +such as `status.state !== "succeeded"` treats permanent failures as work that +is still running. + +#### 14.4 Return a timeout error gracefully + +Use this when a bounded polling loop should return a caller-friendly timeout +instead of exposing a final non-terminal status. The schedule stops recurrence; +Effect code maps the final status into the API contract. + +##### Problem + +The loop should stop when a terminal status is observed and also when its +schedule-side budget is exhausted. If the budget ends while the last observed +status is still non-terminal, return a domain timeout error instead of exposing +a raw `"pending"` value. + +`Schedule.during` does not fail the effect. It only stops allowing future +recurrences, so the timeout error must be produced after `Effect.repeat` +returns. + +Beginner note: Bounds — a schedule-side budget decides when to stop polling. It +does not automatically create a timeout error or interrupt an in-flight poll. + +##### When to use it + +Use it when `"pending"` is normal while polling is open, but a final +`"pending"` means the caller ran out of budget. This is common in job polling, +exports, provisioning, payment settlement, and readiness checks. + +##### When not to use it + +Do not use it to interrupt an in-flight status check. Add `Effect.timeout` to +the checked effect or to the whole workflow when interruption is required. + +Sometimes `"pending"` at the end is still useful data. In that case, keep the +`Effect.repeat` result as the final observed status and let the caller decide +what to do with it. + +Do not map every final status to the same timeout error. A terminal `"failed"` +status and an exhausted polling budget usually mean different things. + +##### Schedule shape + +Keep the latest successful status as the schedule output with +`Schedule.passthrough`, continue only while it is `"pending"`, and combine the +policy with `Schedule.during("30 seconds")`. After `Effect.repeat`, map a final +`"pending"` status to your timeout error. + +##### Example + +```ts +import { Clock, Effect, Fiber, Schedule } from "effect" +import { TestClock } from "effect/testing" + +type JobStatus = + | { readonly state: "pending"; readonly jobId: string } + | { readonly state: "done"; readonly jobId: string; readonly resultId: string } + | { readonly state: "failed"; readonly jobId: string; readonly reason: string } + +type JobTimedOut = { + readonly _tag: "JobTimedOut" + readonly jobId: string +} + +type JobFailed = { + readonly _tag: "JobFailed" + readonly jobId: string + readonly reason: string +} + +const pollForUpTo30Seconds = Schedule.spaced("1 second").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "pending"), + Schedule.bothLeft( + Schedule.during("30 seconds").pipe( + Schedule.satisfiesInputType() + ) + ) +) + +let checks = 0 + +const checkJobStatus = Effect.gen(function*() { + const now = yield* Clock.currentTimeMillis + checks += 1 + + const status: JobStatus = { + state: "pending", + jobId: "job-1" + } + + if (checks <= 3 || now >= 30000) { + console.log(`t+${now}ms check ${checks}: ${status.state}`) + } else if (checks === 4) { + console.log("additional pending checks omitted") + } + + return status +}) + +const pollUntilDoneOrTimeout = checkJobStatus.pipe( + Effect.repeat(pollForUpTo30Seconds), + Effect.flatMap((status): Effect.Effect< + Extract, + JobFailed | JobTimedOut + > => { + switch (status.state) { + case "done": + return Effect.succeed(status) + case "failed": + return Effect.fail( + { + _tag: "JobFailed", + jobId: status.jobId, + reason: status.reason + } satisfies JobFailed + ) + case "pending": + return Effect.fail( + { + _tag: "JobTimedOut", + jobId: status.jobId + } satisfies JobTimedOut + ) + } + }) +) + +const program = Effect.gen(function*() { + const fiber = yield* pollUntilDoneOrTimeout.pipe( + Effect.match({ + onFailure: (error) => ({ _tag: "Failed" as const, error }), + onSuccess: (status) => ({ _tag: "Succeeded" as const, status }) + }), + Effect.forkDetach + ) + + yield* TestClock.adjust("35 seconds") + const result = yield* Fiber.join(fiber) + console.log("result:", result) +}).pipe(Effect.scoped, Effect.provide(TestClock.layer())) + +Effect.runPromise(program) +``` + +The logged result contains `JobTimedOut`. That error is produced by the final +`Effect.flatMap`, not by the schedule. + +##### Variants + +If timeout is an expected business value rather than a failure-channel error, +return a result union from the final mapping step, for example +`{ _tag: "TimedOut", lastStatus }`. + +For strict request deadlines, add a timeout to the status-check effect itself. +That is separate from the schedule-side recurrence budget. + +##### Notes and caveats + +`Effect.repeat` returns the schedule output. With `Schedule.passthrough`, that +output is the final successful status observed by the schedule. + +`Schedule.during("30 seconds")` does not throw, fail, or produce a timeout +error. It stops allowing future recurrences once the elapsed schedule budget is +used up. The budget is checked between successful status checks and does not +interrupt a check that is already running. + +### 15. Adaptive and Fleet-Safe Polling + +#### 15.1 Fast polling during the first few seconds + +Use this for workflows that often settle quickly. The schedule gives the caller +a short responsive burst without making fast polling the steady-state policy. + +##### Problem + +Early completion is common, so waiting through a large interval would feel +unnecessarily slow. The fast cadence should still be bounded, because a +permanent tight loop creates load without adding much value. + +##### When to use it + +Use this when early completion is common and a fresh result is valuable enough +to justify a short burst of extra requests. + +This fits status checks that often move from `"pending"` to `"ready"` shortly +after submission. + +##### When not to use it + +Do not use this as an unbounded polling loop. Fast polling is most useful as an +initial burst, not as the steady-state cadence for long-running work. + +Do not use this to retry a failing status check by itself. With +`Effect.repeat`, failed effects stop the repeat. The schedule only sees +successful status values. + +Do not use a very small interval when each status check is expensive, rate +limited, or likely to queue behind earlier requests. + +##### Schedule shape + +Use `Schedule.spaced("250 millis")` for the burst cadence, `Schedule.take(12)` +to cap the burst, `Schedule.while` to continue only for pending statuses, and +`Schedule.passthrough` to return the latest status. + +##### Example + +```ts +import { Clock, Effect, Fiber, Schedule } from "effect" +import { TestClock } from "effect/testing" + +type Status = + | { readonly state: "pending" } + | { readonly state: "ready"; readonly resourceId: string } + | { readonly state: "failed"; readonly reason: string } + +const fastInitialPolling = Schedule.spaced("250 millis").pipe( + Schedule.take(12), + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "pending") +) + +const script: ReadonlyArray = [ + { state: "pending" }, + { state: "pending" }, + { state: "ready", resourceId: "result-1" } +] + +let checks = 0 + +const checkStatus = Effect.gen(function*() { + const now = yield* Clock.currentTimeMillis + const status = script[Math.min(checks, script.length - 1)]! + checks += 1 + console.log(`t+${now}ms check ${checks}: ${status.state}`) + return status +}) + +const program = Effect.gen(function*() { + const fiber = yield* checkStatus.pipe( + Effect.repeat(fastInitialPolling), + Effect.forkDetach + ) + + yield* TestClock.adjust("3 seconds") + + const finalStatus = yield* Fiber.join(fiber) + console.log("final:", finalStatus) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program) +``` + +The first check is immediate. The 250-millisecond delay applies only after a +successful pending observation. + +##### Variants + +Use a smaller recurrence cap when the first few checks usually settle the +workflow. For example, five recurrences at 200 milliseconds keeps the aggressive +window close to one second after the immediate first check. + +Use a larger interval when requests are heavier or the remote service publishes +status updates less frequently. A 500 millisecond burst can still feel +responsive without creating as much request pressure. + +Use `Schedule.fixed("250 millis")` instead of `Schedule.spaced("250 millis")` +only when you want to target fixed wall-clock boundaries. For most status +endpoints, `Schedule.spaced` is simpler because it waits after each completed +check. + +##### Notes and caveats + +`Schedule.take(12)` limits recurrences after the initial check. It is not a +workflow timeout and it does not interrupt an in-flight request. `Schedule.while` +sees successful status values only. + +#### 15.2 Slow polling after initial responsiveness matters less + +Use this for the slower phase after the initial responsive window has passed. +The caller still observes progress, but the status endpoint is no longer polled +at the early high-frequency cadence. + +##### Problem + +After the first few seconds, polling every few hundred milliseconds usually +creates load without improving the user experience. The policy should slow down +and still stop as soon as a terminal status appears. + +##### When to use it + +Use this when the first responsive phase has passed and the remaining work is +allowed to settle over tens of seconds or minutes. + +This is a good fit for exports, media processing, provisioning, indexing, +settlement checks, and other workflows where early completion is nice, but +later completion does not need instant feedback. + +##### When not to use it + +Do not use this as the whole initial user-facing policy when the first few +seconds are important. The first status check still runs immediately, but the +slow interval controls subsequent recurrences. + +Do not use this when an external system requires a minimum or maximum polling +contract that differs from your chosen interval. + +Do not use this to retry a failing status endpoint by itself. With +`Effect.repeat`, failed effects stop the repeat. The schedule only sees +successful status values. + +##### Schedule shape + +Use `Schedule.spaced("30 seconds")` for the slower cadence, +`Schedule.passthrough` to keep the latest status as the result, and +`Schedule.while` to continue only while that status is still pending. + +##### Example + +```ts +import { Clock, Effect, Fiber, Schedule } from "effect" +import { TestClock } from "effect/testing" + +type Status = + | { readonly state: "pending"; readonly progress: number } + | { readonly state: "ready"; readonly resultId: string } + | { readonly state: "failed"; readonly reason: string } + +const isPending = (status: Status): boolean => status.state === "pending" + +const slowPollingAfterInitialWindow = Schedule.spaced("30 seconds").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => isPending(input)) +) + +const script: ReadonlyArray = [ + { state: "pending", progress: 70 }, + { state: "ready", resultId: "report-42" } +] + +let checks = 0 + +const checkStatus = Effect.gen(function*() { + const now = yield* Clock.currentTimeMillis + const status = script[Math.min(checks, script.length - 1)]! + checks += 1 + console.log(`t+${now}ms check ${checks}: ${status.state}`) + return status +}) + +const program = Effect.gen(function*() { + const fiber = yield* checkStatus.pipe( + Effect.repeat(slowPollingAfterInitialWindow), + Effect.forkDetach + ) + + yield* TestClock.adjust("30 seconds") + + const finalStatus = yield* Fiber.join(fiber) + console.log("final:", finalStatus) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program) +``` + +##### Variants + +Use a shorter interval, such as 10 or 15 seconds, when the user is still +watching the page and a small delay in completion feedback would be noticeable. + +Use a longer interval, such as one or five minutes, when the workflow is mostly +background work and the status endpoint is expensive or rate limited. + +Add jitter when many clients may enter the slow phase at roughly the same time. +The slower cadence reduces load, but it does not by itself prevent synchronized +polling. + +Add a separate cap or elapsed-time budget when the caller needs a definite +answer instead of an open-ended slow wait. + +##### Notes and caveats + +The first check in this phase is immediate. `Schedule.spaced` waits after each +successful status check completes. `Schedule.while` sees successful status +values only; request failures should be retried or reported separately. + +#### 15.3 Polling strategy for user-triggered workflows + +Use this recipe for work started by a user action, such as generating a report, +submitting a review, importing a small file, refreshing derived data, or +starting an approval flow. Poll quickly while the user is likely watching, then +slow down if the workflow is still processing. + +##### Problem + +The first few seconds are important because the user is still watching. If the +workflow finishes quickly, the UI should notice quickly. If it does not finish +quickly, polling should slow down so the status endpoint is not kept under +unnecessary pressure. + +##### When to use it + +Use this when the workflow is user-triggered, visible to the caller, and often +settles shortly after submission. + +This is a good fit for pages that can update from `"processing"` to `"ready"` +without requiring the user to refresh, while still tolerating a slower cadence +after the initial responsive window. + +##### When not to use it + +Do not use this as a general policy for long-running back-office jobs. Those +usually need wider intervals, operational budgets, and separate alerting or +handoff behavior. + +Do not use this when the status endpoint itself is expensive enough that even a +short burst would compete with the workflow being observed. + +Do not use this to retry failed status requests by itself. With +`Effect.repeat`, failed effects stop the repeat. The schedule sees successful +status values. + +##### Schedule shape + +Use `Schedule.andThen` to sequence a short responsive phase into a slower +follow-up phase. Put `Schedule.while` after the sequencing so terminal statuses +stop both phases, and use `Schedule.passthrough` to return the latest +`WorkflowStatus`. + +##### Example + +```ts +import { Clock, Effect, Fiber, Schedule } from "effect" +import { TestClock } from "effect/testing" + +type WorkflowStatus = + | { readonly state: "processing"; readonly message: string } + | { readonly state: "ready"; readonly resultUrl: string } + | { readonly state: "failed"; readonly reason: string } + +const userTriggeredPolling = Schedule.spaced("500 millis").pipe( + Schedule.take(4), + Schedule.andThen(Schedule.spaced("5 seconds")), + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "processing") +) + +const script: ReadonlyArray = [ + { state: "processing", message: "queued" }, + { state: "processing", message: "rendering" }, + { state: "processing", message: "uploading" }, + { state: "processing", message: "still uploading" }, + { state: "processing", message: "almost done" }, + { state: "ready", resultUrl: "/reports/42" } +] + +let checks = 0 + +const checkWorkflowStatus = Effect.gen(function*() { + const now = yield* Clock.currentTimeMillis + const status = script[Math.min(checks, script.length - 1)]! + checks += 1 + console.log(`t+${now}ms check ${checks}: ${status.state}`) + return status +}) + +const program = Effect.gen(function*() { + const fiber = yield* checkWorkflowStatus.pipe( + Effect.repeat(userTriggeredPolling), + Effect.forkDetach + ) + + yield* TestClock.adjust("10 seconds") + + const finalStatus = yield* Fiber.join(fiber) + console.log("final:", finalStatus) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program) +``` + +The first check is immediate. The first four scheduled recurrences use the +500-millisecond cadence; the next recurrence uses the five-second cadence. + +##### Variants + +Use a shorter fast phase for lightweight UI actions where most completions +happen almost immediately. For example, four recurrences at 300 milliseconds +keeps the responsive window brief. + +Use a slower follow-up interval when the user can leave the page open while the +workflow continues. Ten or fifteen seconds is often enough for a visible UI +flow that no longer needs near-instant feedback. + +Add jitter to the slower phase when many users may trigger the same workflow at +the same time, such as after a deploy, notification, or scheduled campaign. + +Add a separate cap or elapsed-time budget when the UI must eventually stop +waiting and tell the user to check back later. + +##### Notes and caveats + +`Schedule.take(4)` limits only the fast recurrence phase. It does not include +the initial status check, and it does not limit the slower phase after +`Schedule.andThen`. + +Apply the status predicate after `Schedule.andThen` so terminal statuses stop +the whole policy, not only the fast phase. + +Keep the status check cheap and read-only. User-triggered polling should +observe progress, not perform the work again. + +Request failures stay in the effect failure channel. `Schedule.while` sees only +successful status values. + +#### 15.4 Polling strategy for long-running back-office jobs + +Use this recipe for back-office jobs that need periodic operator visibility but +are not latency-critical. The schedule gives a few early observations, then +settles into a low-pressure background cadence. + +##### Problem + +Polling too frequently creates steady pressure on the job store, status API, and +worker database. The polling policy should provide enough early signal to catch +fast failures or obvious progress, then settle into a low-pressure cadence until +the job reaches a terminal state. + +##### When to use it + +Use this when job completion is useful to observe but not latency critical. + +This is a good fit for scheduled or queue-driven operational work where the +poller feeds logs, metrics, dashboards, follow-up tasks, or notifications rather +than a user actively watching a page. + +Use it when status checks are cheap enough to run periodically, but expensive +enough that thousands of jobs polling every few seconds would be noticeable. + +##### When not to use it + +Do not use this for interactive workflows where the caller expects immediate +feedback after clicking a button. Those flows usually need a shorter, bounded +early window before moving to background handling. + +Do not use this as a retry policy for a failing status endpoint. With +`Effect.repeat`, failed effects stop the repeat. The schedule sees successful +job status values, not transport or decoding failures. + +Do not leave this as an unbounded poller if the surrounding process has no +lifetime, cancellation, or operational owner. + +##### Schedule shape + +Start with a modest operational cadence, then switch to a slower background +cadence with `Schedule.andThen`. Preserve the latest `JobStatus` with +`Schedule.passthrough`, and continue only while the job is still running. + +##### Example + +```ts +import { Clock, Effect, Fiber, Schedule } from "effect" +import { TestClock } from "effect/testing" + +type JobStatus = + | { readonly state: "running"; readonly processed: number; readonly total: number } + | { readonly state: "completed"; readonly completedAt: string } + | { readonly state: "failed"; readonly reason: string } + +const backOfficeJobPolling = Schedule.spaced("30 seconds").pipe( + Schedule.take(3), + Schedule.andThen(Schedule.spaced("5 minutes")), + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "running") +) + +const script: ReadonlyArray = [ + { state: "running", processed: 10, total: 100 }, + { state: "running", processed: 20, total: 100 }, + { state: "running", processed: 30, total: 100 }, + { state: "running", processed: 40, total: 100 }, + { state: "running", processed: 80, total: 100 }, + { state: "completed", completedAt: "2026-05-17T12:00:00Z" } +] + +let checks = 0 + +const readJobStatus = Effect.gen(function*() { + const now = yield* Clock.currentTimeMillis + const status = script[Math.min(checks, script.length - 1)]! + checks += 1 + console.log(`t+${now}ms check ${checks}: ${status.state}`) + return status +}) + +const program = Effect.gen(function*() { + const fiber = yield* readJobStatus.pipe( + Effect.repeat(backOfficeJobPolling), + Effect.forkDetach + ) + + yield* TestClock.adjust("15 minutes") + + const finalStatus = yield* Fiber.join(fiber) + console.log("final:", finalStatus) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program) +``` + +The example uses three early recurrences to keep the output short. In a real +back-office poller, increase that first-phase count if operators need more early +progress samples. + +##### Variants + +Use a one-minute initial phase when early progress is not operationally useful. +For overnight reconciliation or batch import work, `Schedule.spaced("1 minute")` +followed by `Schedule.spaced("10 minutes")` may be enough. + +Use a shorter steady interval when the poller triggers the next automated step, +such as publishing a completion notification or enqueueing a dependent job. + +Add jitter when many jobs are created at the same scheduled boundary. A slower +cadence reduces pressure, but identical intervals can still synchronize a large +fleet of pollers. + +Add an external timeout, cancellation signal, or owner process lifetime when the +job may remain `"running"` indefinitely because of lost workers or corrupted +state. + +##### Notes and caveats + +`Effect.repeat` runs the status check once before the schedule controls any +recurrence. The first observation is immediate. + +`Schedule.take(3)` limits the first phase to three recurrences after the initial +status check. It is not three total status checks. + +`Schedule.spaced` waits after each successful status check completes. That is +usually what you want for back-office polling because status checks may have +variable latency. + +`Schedule.while` reads successful `JobStatus` values only. Keep status endpoint +failures in the effect error channel and handle retries separately if the +endpoint itself is unreliable. + +#### 15.5 Polling from many clients without synchronization + +Use jitter when many clients poll the same service on a regular cadence, but no +client needs to land on an exact shared boundary. Jitter keeps the interval +recognizable while making each recurrence delay vary slightly. + +##### Problem + +If many clients start together and all poll every five seconds, they can keep +calling the status endpoint in waves. The average request rate may be fine, but +the service sees short synchronized bursts instead of a steadier stream. + +Jitter is a small random adjustment to each recurrence delay. It does not change +what status means or when polling should stop; it only reduces accidental timing +alignment. + +##### When to use it + +Use this for independent clients, fibers, workers, or browser sessions that poll +the same read-only status endpoint. + +It fits work that is already in progress, where each caller has its own id and +periodically asks whether the remote state has changed. + +##### When not to use it + +Do not use jitter as a stop condition. Polling still needs a status predicate, +timeout, recurrence cap, or external interruption. + +Do not use it for clock-aligned work, such as checks that must run exactly at +the top of each minute. + +Do not treat client-side jitter as overload control. Rate limits, admission +control, quotas, and server-side load shedding are separate mechanisms. + +##### Schedule shape + +Start with `Schedule.spaced` for the base interval, apply +`Schedule.jittered`, preserve the latest status with `Schedule.passthrough`, +and stop with `Schedule.while` once the status is no longer pending. + +In Effect, `Schedule.jittered` adjusts each delay to between 80% and 120% of +the original delay. A five-second interval becomes a recurrence delay between +four and six seconds. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type Status = + | { readonly state: "pending"; readonly requestId: string } + | { readonly state: "complete"; readonly requestId: string; readonly resultId: string } + +const scriptedStatuses: ReadonlyArray = [ + { state: "pending", requestId: "request-42" }, + { state: "pending", requestId: "request-42" }, + { state: "complete", requestId: "request-42", resultId: "result-7" } +] + +let readIndex = 0 + +const checkStatus = (requestId: string): Effect.Effect => + Effect.sync(() => { + const status = scriptedStatuses[ + Math.min(readIndex, scriptedStatuses.length - 1) + ]! + readIndex += 1 + return status + }).pipe( + Effect.tap((status) => Console.log(`[${requestId}] observed ${status.state}`)) + ) + +const pollWithJitter = Schedule.spaced("20 millis").pipe( + Schedule.jittered, + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "pending") +) + +const program = checkStatus("request-42").pipe( + Effect.repeat(pollWithJitter), + Effect.tap((status) => Console.log(`finished with ${status.state}`)) +) + +Effect.runPromise(program).then((status) => { + console.log("result:", status) +}) +// Output: +// [request-42] observed pending +// [request-42] observed pending +// [request-42] observed complete +// finished with complete +// result: { state: 'complete', requestId: 'request-42', resultId: 'result-7' } +``` + +The first status check runs immediately. Later checks wait for the jittered +delay, and the repeat stops as soon as the latest successful status is no longer +`"pending"`. + +##### Variants + +Add `Schedule.take` or combine with `Schedule.recurs` when the caller needs a +hard recurrence limit. Interpret the last status explicitly, because a bounded +schedule can stop while the operation is still pending. + +Use a shorter base interval for cheap status checks that need quick feedback. +Use a longer interval when the dependency should receive less polling traffic. + +If the status request itself can fail transiently, retry that request separately +before repeating it. `Effect.repeat` feeds successful status values into the +schedule; failures stop the repeat unless handled first. + +##### Notes and caveats + +`Schedule.jittered` has fixed bounds: 80% to 120% of the original delay. + +`Schedule.while` sees successful status values only. It does not classify +transport, decoding, authorization, or service failures from the effect error +channel. + +When a timing schedule reads the latest status through `metadata.input`, apply +`Schedule.satisfiesInputType()` before `Schedule.while`. + +#### 15.6 Jittered status checks in distributed systems + +Distributed workers often need regular status checks, but a whole fleet should +not call the same dependency at the same instant. Jitter keeps each worker near +the intended cadence while letting checks drift apart. + +##### Problem + +Workers may check leases, shard assignments, replication tasks, queue drains, or +long-running operations owned by another service. A fixed interval is simple, +but workers that restart together or receive the same batch together can remain +synchronized. + +Use jitter when the exact second is not important and the operational goal is a +steadier stream of status reads. + +##### When to use it + +Use this when multiple replicas, workers, or service instances poll the same +kind of status endpoint. + +It fits checks that should happen regularly, but where one worker checking a +little earlier or later has no semantic meaning. + +##### When not to use it + +Do not use jitter as a completion rule. The status value still decides whether +the remote work is active or terminal. + +Do not use it for exact boundary checks, coordinated leader actions, or jobs +where all instances intentionally sample at the same time. + +Do not use it as a replacement for concurrency limits, quotas, or backpressure +when the dependency has hard capacity limits. + +##### Schedule shape + +Combine a base polling interval with `Schedule.jittered`, preserve the latest +status using `Schedule.passthrough`, and continue only while the status is still +active. + +Effect's `Schedule.jittered` changes each recurrence delay to 80% to 120% of +the original delay. A ten-second interval becomes a delay between eight and +twelve seconds. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type WorkerStatus = + | { readonly state: "running"; readonly workerId: string; readonly taskId: string } + | { readonly state: "complete"; readonly workerId: string; readonly taskId: string } + +const scriptedStatuses: ReadonlyArray = [ + { state: "running", workerId: "worker-a", taskId: "task-9" }, + { state: "running", workerId: "worker-a", taskId: "task-9" }, + { state: "complete", workerId: "worker-a", taskId: "task-9" } +] + +let readIndex = 0 + +const checkWorkerStatus = ( + workerId: string, + taskId: string +): Effect.Effect => + Effect.sync(() => { + const status = scriptedStatuses[ + Math.min(readIndex, scriptedStatuses.length - 1) + ]! + readIndex += 1 + return status + }).pipe( + Effect.tap((status) => Console.log(`[${workerId}/${taskId}] ${status.state}`)) + ) + +const distributedStatusChecks = Schedule.spaced("25 millis").pipe( + Schedule.jittered, + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "running") +) + +const program = checkWorkerStatus("worker-a", "task-9").pipe( + Effect.repeat(distributedStatusChecks), + Effect.tap((status) => Console.log(`final status: ${status.state}`)) +) + +Effect.runPromise(program).then((status) => { + console.log("result:", status) +}) +// Output: +// [worker-a/task-9] running +// [worker-a/task-9] running +// [worker-a/task-9] complete +// final status: complete +// result: { state: 'complete', workerId: 'worker-a', taskId: 'task-9' } +``` + +Each worker evaluates its own schedule. Even if several workers start together, +later checks choose independent jittered delays around the same base interval. + +##### Variants + +Use a shorter base interval for cheap local checks. Use a longer interval for +shared databases, control services, or external APIs. + +Add a recurrence cap when a worker should stop after a bounded number of active +observations. Treat the final active status as "not finished in time" rather +than as success. + +Retry transient failures inside the status-check effect when appropriate. +`Effect.repeat` itself repeats successes; it does not turn failed reads into +status values. + +##### Notes and caveats + +`Schedule.jittered` does not expose configurable bounds; the range is fixed at +80% to 120%. + +The first status check runs immediately. The schedule controls only later +recurrences. + +Use `Schedule.satisfiesInputType()` before `Schedule.while` when the +predicate reads the latest successful status from `metadata.input`. + +#### 15.7 Reduce herd effects in control planes + +A herd effect is many independent callers hitting the same dependency at the +same time. In control planes, jitter is a small scheduling tool that helps keep +status polling from turning into synchronized bursts. + +##### Problem + +Control planes often expose status for deployment rollouts, cluster membership, +workflow progress, assignment health, or reconciliation. After restarts, +incident recovery, autoscaling, or batch submissions, many callers may begin +polling together and remain aligned on fixed interval boundaries. + +Jitter does not make polling rare. It keeps the intended cadence while making +each caller's recurrence delay slightly different. + +##### When to use it + +Use this when many processes, workers, tenants, or browser sessions poll a +control-plane endpoint for read-only status. + +It fits cases where a response that is a second early or late is fine, but +synchronized read bursts are expensive. + +##### When not to use it + +Do not use jitter when a control-plane action must happen on an exact +wall-clock boundary. + +Do not use jitter as the completion rule. Status values still decide whether an +operation is queued, reconciling, ready, or rejected. + +Do not treat jitter as a complete overload strategy. Admission control, quotas, +server-side rate limits, and deployment pacing remain separate concerns. + +##### Schedule shape + +Use a normal control-plane polling interval with `Schedule.spaced`, apply +`Schedule.jittered`, preserve the latest status with `Schedule.passthrough`, +and keep polling only while the operation is active. + +Effect's jitter range is fixed at 80% to 120%. A fifteen-second interval becomes +a delay between twelve and eighteen seconds. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type ControlPlaneStatus = + | { readonly state: "queued"; readonly operationId: string } + | { readonly state: "reconciling"; readonly operationId: string } + | { readonly state: "ready"; readonly operationId: string } + | { readonly state: "rejected"; readonly operationId: string; readonly reason: string } + +const scriptedStatuses: ReadonlyArray = [ + { state: "queued", operationId: "op-22" }, + { state: "reconciling", operationId: "op-22" }, + { state: "ready", operationId: "op-22" } +] + +let readIndex = 0 + +const isActive = (status: ControlPlaneStatus): boolean => status.state === "queued" || status.state === "reconciling" + +const describeOperation = ( + operationId: string +): Effect.Effect => + Effect.sync(() => { + const status = scriptedStatuses[ + Math.min(readIndex, scriptedStatuses.length - 1) + ]! + readIndex += 1 + return status + }).pipe( + Effect.tap((status) => Console.log(`[${operationId}] ${status.state}`)) + ) + +const controlPlanePolling = Schedule.spaced("30 millis").pipe( + Schedule.jittered, + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => isActive(input)) +) + +const program = describeOperation("op-22").pipe( + Effect.repeat(controlPlanePolling), + Effect.tap((status) => Console.log(`control-plane result: ${status.state}`)) +) + +Effect.runPromise(program).then((status) => { + console.log("result:", status) +}) +// Output: +// [op-22] queued +// [op-22] reconciling +// [op-22] ready +// control-plane result: ready +// result: { state: 'ready', operationId: 'op-22' } +``` + +Each caller chooses its own adjusted delay on each recurrence. Even when callers +start together, later status checks are less likely to stay aligned. + +##### Variants + +Use longer base intervals for expensive control-plane reads or operations that +normally take minutes. Use shorter intervals only for cheap endpoints where +tighter observation is worth the load. + +Add a bounded schedule when callers must stop observing non-terminal operations. +Return a distinct "still active" outcome if the bound stops polling before the +control plane reaches a terminal state. + +If the control-plane read can fail transiently, add a retry policy to that read +before the repeat. + +##### Notes and caveats + +Client-side jitter reduces accidental alignment among cooperative callers. It +does not protect the control plane from malicious clients, hard capacity +limits, or every caller being triggered by the same external event. + +`Effect.repeat` repeats after successful status reads. Failed reads stop the +repeat unless recovered first. + +Use `Schedule.satisfiesInputType()` before reading `metadata.input` in +`Schedule.while`. + +## Part V — Delay, Backoff, and Load Control + +### 16. Choose a Delay Strategy + +#### 16.1 Constant delays + +A constant delay waits the same amount of time before each retry or repeated +iteration. It keeps timing predictable without introducing an adaptive backoff +curve. + +##### Problem + +You need a visible pause between attempts, but the dependency does not need +progressively increasing delays. Immediate retries are too aggressive, while +exponential backoff would obscure a deliberately steady cadence. The policy +should say two things clearly: + +- how long to wait between attempts +- when to stop retrying + +##### When to use it + +Use a constant delay for stable dependencies that occasionally return temporary +failures: a local service restarting, a short network hiccup, a lock that clears +quickly, or an idempotent request to a dependency that normally recovers within +a few seconds. + +It is also useful as a conservative first production policy. The delay is easy +to explain in logs and dashboards, and changing `"250 millis"` to `"1 second"` +does not change the shape of the schedule. + +##### When not to use it + +Do not use a constant delay as the only protection for overload. If every retry +waits the same amount of time, a busy caller can keep applying steady pressure +to a dependency that is already failing. + +Do not use it without a stop condition unless the workflow is intentionally +unbounded. `Schedule.spaced("1 second")` by itself keeps recurring forever. + +Do not use it for unsafe side effects. Retrying writes requires idempotency, +deduplication, or a domain-specific recovery plan before the schedule is chosen. + +##### Schedule shape + +For retrying with a constant delay, start with `Schedule.spaced(duration)` and +combine it with a limit such as `Schedule.recurs(n)`. + +`Schedule.spaced(duration)` waits that duration after each completed attempt +before allowing the next recurrence. Use this for ordinary retry spacing and +for repeat loops where the gap after work completes is what matters. + +`Schedule.fixed(duration)` is different: it targets fixed interval boundaries. +That is useful for fixed-cadence repeating work, but it is usually not what you +mean by "wait 500 milliseconds before retrying." For retry policies, reach for +`spaced` first unless you specifically need clock-like cadence. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class TemporaryProfileError extends Data.TaggedError("TemporaryProfileError")<{ + readonly reason: "Timeout" | "Unavailable" +}> {} + +let attempts = 0 + +const fetchProfile = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`profile attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail( + new TemporaryProfileError({ reason: "Unavailable" }) + ) + } + + return { id: "user-123", name: "Ada" } +}) + +const retryWithConstantDelay = Schedule.spaced("50 millis").pipe( + Schedule.both(Schedule.recurs(4)) +) + +const program = fetchProfile.pipe( + Effect.retry(retryWithConstantDelay) +) + +Effect.runPromise(program).then((profile) => { + console.log(`loaded profile: ${profile.name}`) +}) +// Output: +// profile attempt 1 +// profile attempt 2 +// profile attempt 3 +// loaded profile: Ada +``` + +The example uses a short delay so it terminates quickly in `scratchpad/repro.ts`. +In a real retry, choose the delay from the dependency's recovery behavior. + +##### Variants + +For a user-facing request, keep both the delay and the retry count small so the +caller gets an answer quickly. For a background worker, increase the delay +before increasing the retry count. That keeps the policy simple while reducing +pressure on the dependency. + +If many instances run the same policy at the same time, a constant delay can +synchronize retries. Add jitter only after the base delay and retry limit are +correct. + +##### Notes and caveats + +`Effect.retry` feeds typed failures into the schedule. The first execution is +not delayed, and defects or interruptions are not retried as ordinary typed +failures. + +The output of `Schedule.spaced` is a recurrence count. In a retry, that output +is used to drive the policy; the successful value of the retried effect is what +the program returns. + +Keep classification close to the effect being retried. The schedule should +describe timing and limits, while the domain code decides which failures are +safe to retry. + +#### 16.2 Linear backoff + +Linear backoff adds the same amount of extra delay at each retry decision. It +reduces pressure gradually while keeping the delay curve easier to explain than +exponential backoff. + +Effect does not provide a `Schedule.linear` constructor. Build this policy from +a stateful schedule that counts retry decisions, then derive the delay from +that count. + +##### Problem + +A worker calls an internal dependency that usually recovers within a few +seconds. You want waits such as 250 milliseconds, 500 milliseconds, 750 +milliseconds, and 1 second before giving up. Doubling would make later attempts +too far apart for this workflow. + +##### When to use it + +Use linear backoff when each failure should reduce pressure, but you still want +predictable recovery speed. It fits short-lived overload, brief queue or cache +contention, reconnect attempts inside a single process, and internal services +where a simple fixed increment is easier to reason about than an exponential +curve. + +##### When not to use it + +Do not use linear backoff to retry permanent failures. Authentication errors, +validation failures, malformed requests, and unsafe non-idempotent writes should +be handled before the retry policy is applied. + +Do not use it as a fleet-wide protection mechanism by itself. If many callers +fail together, a deterministic linear policy can still make them retry together. +For clustered systems or public APIs, consider adding jitter after choosing the +base delay curve. + +Do not leave the schedule unbounded unless retrying forever is intentional. A +linear delay grows slowly, so an unbounded policy can keep work alive for a long +time. + +##### Schedule shape + +`Schedule.unfold(initial, next)` outputs the current state and computes the next +state for the following decision. Starting at `1` makes the first retry delay +one increment instead of zero. + +`Schedule.addDelay` adds an extra delay based on the schedule output. Because +`Schedule.unfold` has no delay of its own, the added delay becomes the retry +delay. + +`Schedule.take(5)` bounds the schedule so the effect can retry only a limited +number of times after the original attempt. + +##### Example + +```ts runnable deterministic +import { Console, Data, Duration, Effect, Schedule } from "effect" + +class IndexError extends Data.TaggedError("IndexError")<{ + readonly reason: "busy" | "unavailable" +}> {} + +let attempts = 0 + +const refreshSearchIndex = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`index attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail(new IndexError({ reason: "busy" })) + } + + return "index refreshed" +}) + +const retryWithLinearBackoff = Schedule.unfold( + 1, + (step) => Effect.succeed(step + 1) +).pipe( + Schedule.addDelay((step) => Effect.succeed(Duration.millis(step * 20))), + Schedule.take(5) +) + +const program = refreshSearchIndex.pipe( + Effect.retry(retryWithLinearBackoff) +) + +Effect.runPromise(program).then((message) => { + console.log(message) +}) +// Output: +// index attempt 1 +// index attempt 2 +// index attempt 3 +// index attempt 4 +// index refreshed +``` + +The example uses a 20 millisecond increment so it finishes quickly. With a 250 +millisecond increment, the same shape would wait 250ms, 500ms, 750ms, and so on. +If all attempts fail, `Effect.retry` returns the last `IndexError`. + +##### Variants + +Use a smaller increment for user-facing paths where responsiveness matters. Use +a larger increment for background work that should reduce downstream pressure +more visibly. If many processes may retry at the same time, add +`Schedule.jittered` to the finished policy. + +##### Notes and caveats + +The step value is schedule state, not the result of the retried effect. +`Effect.retry` feeds typed failures into the schedule, but this policy ignores +the failure value and only uses the retry count. + +Because the delay is computed from the step value, changing the initial state +changes the first delay. Start at `0` only when an immediate first retry is +intentional. + +Linear backoff has no built-in cap. If the retry count can become large, add a +limit such as `Schedule.take`, a time budget, or a maximum-delay policy before +using it in production. + +#### 16.3 Exponential backoff + +Exponential backoff grows the delay after each failed attempt. In Effect, use +`Schedule.exponential` for that growing delay and compose it with an explicit +limit so the retry policy has a clear end. + +##### Problem + +An HTTP API returns a temporary 503, a database is failing over, or a queue +broker is recovering after a restart. Retrying immediately can make the outage +worse. Retrying at a fixed interval can still keep too much steady pressure on +the dependency. + +Use exponential backoff when the first retry should happen soon, later retries +should slow down aggressively, and the whole policy should stop after a known +number of retries. + +##### When to use it + +Use exponential backoff for idempotent remote calls where failures are likely +to be temporary and downstream recovery matters. It is a practical default for +timeouts, connection resets, brief unavailability, and overload responses that +should not be hammered by a tight loop. + +The backoff should be visible in the schedule value. A reviewer should be able +to see the starting delay, the growth behavior, and the retry limit without +searching for sleeps or counters elsewhere in the code. + +##### When not to use it + +Do not use exponential backoff to retry permanent errors. Validation failures, +authorization failures, malformed requests, and unsafe non-idempotent writes +should be handled before this policy is applied. + +Do not use `Schedule.exponential` by itself as a production retry policy unless +unbounded retrying is intentional. The schedule keeps recurring, so add +`Schedule.recurs`, `Schedule.take`, or another stopping condition. + +##### Schedule shape + +`Schedule.exponential(base)` waits using `base * factor^n`, with a default +factor of `2`. For example, `Schedule.exponential("100 millis")` produces +delays of 100 milliseconds, 200 milliseconds, 400 milliseconds, 800 +milliseconds, and so on. + +With `Effect.retry`, the first call runs immediately. If it fails with a typed +error, the schedule decides whether to retry and how long to wait before the +next call. + +Combine `Schedule.exponential(base)` with `Schedule.recurs(n)` to keep the +growing delay but bound the number of retries. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class DownstreamError extends Data.TaggedError("DownstreamError")<{ + readonly reason: "Timeout" | "Unavailable" | "Overloaded" +}> {} + +let attempts = 0 + +const fetchCustomerProfile = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`profile API attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail(new DownstreamError({ reason: "Unavailable" })) + } + + return { customerId: "customer-123", plan: "pro" as const } +}) + +const retryTransientRemoteFailure = Schedule.exponential("20 millis").pipe( + Schedule.both(Schedule.recurs(5)) +) + +const program = fetchCustomerProfile.pipe( + Effect.retry(retryTransientRemoteFailure) +) + +Effect.runPromise(program).then((profile) => { + console.log(`${profile.customerId} plan: ${profile.plan}`) +}) +// Output: +// profile API attempt 1 +// profile API attempt 2 +// profile API attempt 3 +// profile API attempt 4 +// customer-123 plan: pro +``` + +The example uses 20 milliseconds as the base so it finishes quickly. With a +100 millisecond base, the first five retry delays would be 100ms, 200ms, 400ms, +800ms, and 1600ms. If all retries fail, `Effect.retry` returns the last +`DownstreamError`. + +##### Variants + +Use a gentler factor, such as `Schedule.exponential("200 millis", 1.5)`, when +doubling backs off too quickly for the workflow. For repeated successful work, +`Schedule.take` can limit how many schedule outputs are used. + +##### Notes and caveats + +`Schedule.recurs(5)` means five retries after the original attempt, so the +effect can run up to six times total. + +Basic exponential backoff has no maximum delay cap and no jitter. For +user-facing flows, long-running workers, large fleets, or rate-limited APIs, +add the appropriate cap, time budget, or jittered policy in the surrounding +recipe. + +The schedule controls recurrence mechanics. It does not decide whether a +domain operation is safe to retry; classify errors and ensure idempotency near +the effect being retried. + +#### 16.4 Capped exponential backoff + +Capped exponential backoff grows retry delays quickly at first, then stops +increasing once they reach an operational maximum. The cap keeps a caller, +worker, or supervisor from waiting minutes or hours between attempts. + +##### Problem + +An operation can tolerate short exponential delays at the start of an outage, +but not the long tail of an uncapped curve. A request timeout, queue lease, +reconnect loop, or operational alert window may require every retry decision to +stay below a known maximum. + +##### When to use it + +Use capped exponential backoff when the first few retries should spread out +quickly, but every later retry still needs to happen within a known maximum +interval. + +This is a common fit for idempotent calls to HTTP APIs, databases, queues, +caches, and control planes. The cap gives operators a concrete answer to "how +long can this wait between attempts?" while preserving the load-shedding +benefit of exponential growth. + +##### When not to use it + +Do not use this policy to make unsafe work retryable. Non-idempotent writes need +idempotency keys, deduplication, transactions, or another domain guarantee +before retrying is safe. + +Do not treat the cap as a total timeout. A policy capped at 5 seconds can still +spend much longer overall if it allows many retries. Use a retry limit or a +time budget when the whole operation must finish within a bound. + +Do not use the same capped curve across a large fleet without thinking about +synchronization. If many clients fail together, add jitter after the base timing +is correct. + +##### Schedule shape + +Start with `Schedule.exponential(base)`. It returns a schedule whose output is +the current delay and whose delay grows by the exponential factor. + +Use `Schedule.modifyDelay` to clamp each computed delay before it is used. Add +a retry limit separately with `Schedule.both(Schedule.recurs(n))` when the +operation should eventually give up. + +##### Example + +```ts runnable deterministic +import { Console, Data, Duration, Effect, Schedule } from "effect" + +class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{ + readonly service: string +}> {} + +let attempts = 0 + +const refreshControlPlaneState = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`control-plane attempt ${attempts}`) + + if (attempts < 5) { + return yield* Effect.fail( + new ServiceUnavailable({ service: "control-plane" }) + ) + } + + return "control plane refreshed" +}) + +const cappedBackoff = Schedule.exponential("20 millis").pipe( + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.millis(50)))), + Schedule.both(Schedule.recurs(8)) +) + +const program = refreshControlPlaneState.pipe( + Effect.retry(cappedBackoff) +) + +Effect.runPromise(program).then((message) => { + console.log(message) +}) +// Output: +// control-plane attempt 1 +// control-plane attempt 2 +// control-plane attempt 3 +// control-plane attempt 4 +// control-plane attempt 5 +// control plane refreshed +``` + +The first call to `refreshControlPlaneState` runs immediately. If it fails with +`ServiceUnavailable`, retries use exponential delays starting at 20 +milliseconds. Each delay is capped at 50 milliseconds in the example. A +production policy might use a 5 second or 30 second cap, depending on the +workflow. + +##### Variants + +Use a smaller cap for interactive work and a larger cap for background recovery. +If many processes may retry the same dependency together, keep the cap and add +`Schedule.jittered`. + +##### Notes and caveats + +`Schedule.modifyDelay` changes the delay chosen by the schedule. It does not +change the schedule output. For `Schedule.exponential`, the output remains the +uncapped exponential duration, even though the actual wait has been capped. + +`Schedule.recurs(8)` means eight retries after the original attempt, not eight +total attempts. + +With `Effect.retry`, failures are fed into the schedule. If the schedule stops, +the last typed failure is returned. If any attempt succeeds, the retry policy is +finished and the successful value is returned. + +### 17. Operational Backoff Recipes + +#### 17.1 Backoff for unstable remote APIs + +Remote APIs can fail for temporary reasons: gateway timeouts, short rate-limit +windows, deploys, or overloaded dependencies behind the endpoint. A bounded +exponential backoff gives the service time to recover while keeping retry load +explicit. + +##### Problem + +You submit usage events to a billing API. The request is safe to retry because +it uses an idempotency key, but the API sometimes returns retryable statuses +such as `408`, `429`, or `5xx`. + +The policy should start with a short delay, grow exponentially, cap long waits, +stop after a small budget, and avoid retrying permanent client errors. + +##### When to use it + +Use this for idempotent remote calls: fetching a report, submitting a +deduplicated event, refreshing a token from a temporarily unavailable identity +provider, or calling an internal service that occasionally returns `503`. + +It is useful when many callers share the dependency because the retry count, +elapsed budget, cap, and jitter are visible in one schedule. + +##### When not to use it + +Do not retry bad input, missing credentials, forbidden access, nonexistent +resources, or schema mismatches. Be careful with non-idempotent operations: +backoff controls timing, not duplicate side effects. + +##### Schedule shape + +`Schedule.exponential("100 millis")` produces delays that grow by the default +factor of `2`. Add `Schedule.jittered` when many clients may fail together. Use +`Schedule.modifyDelay` with `Duration.min` to cap each delay, then combine the +cadence with `Schedule.recurs` and `Schedule.during` for count and time bounds. + +Use the `while` option on `Effect.retry` to classify retryable errors. + +##### Example + +```ts +import { Console, Data, Duration, Effect, Schedule } from "effect" + +class RemoteApiError extends Data.TaggedError("RemoteApiError")<{ + readonly status: number + readonly message: string +}> {} + +interface UsageReceipt { + readonly id: string +} + +interface UsageRequest { + readonly accountId: string + readonly units: number + readonly idempotencyKey: string +} + +const statuses = [503, 429, 200] as const +let attempts = 0 + +const submitUsageEvent = (request: UsageRequest) => + Effect.gen(function*() { + attempts += 1 + const status = statuses[Math.min(attempts - 1, statuses.length - 1)] + yield* Console.log(`billing attempt ${attempts}: HTTP ${status}`) + + if (status !== 200) { + return yield* Effect.fail( + new RemoteApiError({ status, message: "temporary billing failure" }) + ) + } + + return { + id: `receipt-${request.idempotencyKey}` + } satisfies UsageReceipt + }) + +const isRetryable = (error: RemoteApiError) => error.status === 408 || error.status === 429 || error.status >= 500 + +const remoteApiBackoff = Schedule.exponential("20 millis").pipe( + Schedule.jittered, + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.millis(80)))), + Schedule.both(Schedule.recurs(4)), + Schedule.both(Schedule.during("1 second")) +) + +const program = Effect.gen(function*() { + const receipt = yield* submitUsageEvent({ + accountId: "acct_123", + units: 42, + idempotencyKey: "usage-acct_123-demo" + }).pipe( + Effect.retry({ + schedule: remoteApiBackoff, + while: isRetryable + }) + ) + yield* Console.log(`accepted usage event: ${receipt.id}`) +}).pipe( + Effect.catch((error) => Console.log(`usage event failed without retrying further: ${error._tag}`)) +) + +Effect.runPromise(program) +``` + +The example uses millisecond-scale delays so it is quick to run. Increase the +base, cap, and budget for a real remote API. + +##### Variants + +For a user-facing request, shorten the elapsed budget and retry count. For a +background worker, keep jitter enabled and emit metrics at the retry boundary so +operators can see when the dependency is forcing callers into backoff. + +If the API returns `Retry-After`, prefer that server-provided timing for rate +limits. Exponential backoff is a local fallback when the remote service gives no +better signal. + +##### Notes and caveats + +`Schedule.exponential` recurs forever by itself. Always pair it with a count +limit, elapsed budget, or domain predicate. + +Backoff is only one part of remote API safety. Use timeouts, classify errors, +keep request bodies replayable, and require idempotency for mutating calls. + +#### 17.2 Backoff for queue reconnection + +Queue reconnection should have one visible timing policy. That policy describes +how much pressure a consumer applies while the broker, network path, or endpoint +is recovering. + +##### Problem + +A worker must open a queue connection before it can consume messages. The first +attempt should happen immediately. Transient connection failures should retry +with a growing delay and stop after a clear budget. + +##### When to use it + +Use this for queue clients, broker consumers, or background workers where the +right response to a transient disconnect is to reconnect. Operators should be +able to answer "how many reconnects will this try?" and "how quickly does the +delay grow?" from the schedule. + +##### When not to use it + +Do not retry permanent configuration problems: bad credentials, missing queues, +invalid consumer groups, or schema mismatches. Keep decode and processing +failures out of the reconnect policy unless reconnecting is truly the recovery +action. + +##### Schedule shape + +`Schedule.exponential("250 millis")` starts at 250 milliseconds and doubles by +default. `Schedule.recurs(6)` allows six retries after the original attempt. +`Schedule.jittered` spreads reconnects when many workers fail at the same time. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type QueueConnectError = + | { readonly _tag: "BrokerUnavailable" } + | { readonly _tag: "ConnectionReset" } + +type QueueRuntimeError = + | QueueConnectError + | { readonly _tag: "MessageDecodeFailed" } + +interface QueueConnection { + readonly run: Effect.Effect +} + +let connectAttempts = 0 + +const openQueueConnection: Effect.Effect = Effect.gen(function*() { + connectAttempts += 1 + yield* Console.log(`queue connect attempt ${connectAttempts}`) + + if (connectAttempts < 3) { + return yield* Effect.fail({ _tag: "BrokerUnavailable" } as const) + } + + return { + run: Console.log("consumer processed one message") + } +}) + +const reconnectBackoff = Schedule.exponential("20 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const connectWithBackoff = openQueueConnection.pipe( + Effect.retry(reconnectBackoff) +) + +const consumer = Effect.gen(function*() { + const connection = yield* connectWithBackoff + yield* connection.run +}).pipe( + Effect.catch((error) => Console.log(`consumer failed: ${error._tag}`)) +) + +Effect.runPromise(consumer) +// Output: +// queue connect attempt 1 +// queue connect attempt 2 +// queue connect attempt 3 +// consumer processed one message +``` + +The example stops after one processed message so it can be pasted into a +scratchpad and run immediately. + +##### Variants + +For a single local worker, deterministic timing may be easier to debug, so +jitter can be removed. For a larger fleet, keep jitter and consider a larger +base delay so broker recovery does not receive a synchronized reconnect wave. + +For a supervisor that should restart the whole consumer after runtime +disconnects, apply the policy around the larger effect that opens the connection +and runs the consume loop. + +##### Notes and caveats + +`Effect.retry` feeds typed failures into the schedule. In the example, only +`QueueConnectError` reaches the reconnect policy, so message decode failures are +not silently treated as connection problems. + +#### 17.3 Backoff for cold-start dependencies + +Cold-start checks should be responsive when dependencies are ready and gentle +when they are not. During deploys or scale-out, many instances may open pools, +load config, warm caches, and contact dependencies at the same time. + +##### Problem + +A startup readiness check controls whether the process becomes ready. It should +run immediately, retry only transient readiness failures, increase delay after +each failed check, and stop after a clear startup budget. + +##### When to use it + +Use this for idempotent startup gates: pinging a database, checking a cache +endpoint, opening a broker connection, or asking a local sidecar whether it has +finished initialization. + +It is most useful when process start order does not guarantee dependency +readiness: deploys, autoscaling, local multi-service development, and test +containers. + +##### When not to use it + +Do not retry bad credentials, invalid URLs, missing schemas, unsupported +protocol versions, or authorization failures. Retry the narrow readiness check, +not the whole startup program. + +If every deploy overloads the dependency for minutes, the schedule is exposing a +capacity problem rather than solving it. + +##### Schedule shape + +`Schedule.exponential("200 millis")` waits 200 milliseconds before the first +retry, then 400 milliseconds, 800 milliseconds, and so on. Combine it with +`Schedule.recurs` for a startup budget. Use `Schedule.jittered` for fleet +startup so instances are less likely to retry at exactly the same moment. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class DependencyNotReady extends Data.TaggedError("DependencyNotReady")<{ + readonly dependency: string + readonly reason: string +}> {} + +class DependencyMisconfigured extends Data.TaggedError("DependencyMisconfigured")<{ + readonly dependency: string + readonly reason: string +}> {} + +type StartupDependencyError = DependencyNotReady | DependencyMisconfigured + +let readinessChecks = 0 + +const checkDatabaseReady: Effect.Effect = Effect.gen(function*() { + readinessChecks += 1 + yield* Console.log(`database readiness check ${readinessChecks}`) + + if (readinessChecks < 4) { + return yield* Effect.fail( + new DependencyNotReady({ + dependency: "postgres", + reason: "accepting connections soon" + }) + ) + } +}) + +const startHttpServer = Console.log("HTTP server started") + +const coldStartBackoff = Schedule.exponential("15 millis").pipe( + Schedule.both(Schedule.recurs(5)), + Schedule.jittered +) + +const isRetryableStartupFailure = (error: StartupDependencyError) => error._tag === "DependencyNotReady" + +const program = Effect.gen(function*() { + yield* checkDatabaseReady.pipe( + Effect.retry({ + schedule: coldStartBackoff, + while: isRetryableStartupFailure + }) + ) + + yield* startHttpServer +}).pipe( + Effect.catch((error) => Console.log(`startup failed: ${error._tag}`)) +) + +Effect.runPromise(program) +// Output: +// database readiness check 1 +// database readiness check 2 +// database readiness check 3 +// database readiness check 4 +// HTTP server started +``` + +The first readiness check runs before the schedule is consulted. If a retry +eventually succeeds, startup continues. If the failure is misconfiguration, the +`while` predicate prevents retrying. + +##### Variants + +For a single local process, remove jitter when deterministic timing is more +useful. For large rollouts, keep jitter and use a slower base delay or gentler +growth factor. For dependencies with a strict startup service-level objective, +use a smaller retry count and let orchestration restart or reschedule the +process after failure. + +##### Notes and caveats + +`Schedule.recurs(5)` means five retries after the original readiness check, not +five total checks. + +Backoff reduces startup storms by waiting longer after each failure. Jitter +reduces synchronization between instances. They address different parts of the +same startup problem and are often used together. + +#### 17.4 Cap long tails in retry behavior + +Use this to keep late retries visible by putting a maximum wait on a growing +backoff policy. + +##### Problem + +Long retry tails make systems look idle while work is still pending. A worker +may be holding a queue lease, a supervisor may be waiting for reconnect, or an +operator may be looking for the next attempt in logs. A cap keeps the tail +within a known interval. + +##### When to use it + +Use it for idempotent retry paths such as control-plane calls, reconnect loops, +queue consumers, and reconciliation jobs. The cap answers "how long until this +tries again?" and a separate retry limit answers "when does this stop?" + +##### When not to use it + +Do not use a cap to make permanent failures look transient. Classify validation +errors, authorization failures, malformed requests, and unsafe writes before the +retry policy is applied. + +Do not treat the cap as a total timeout. A 5-second cap only bounds the delay +between attempts. The total runtime also depends on how many retries are allowed +and how long each attempted operation takes. + +##### Schedule shape + +Start with `Schedule.exponential(base)`, then clamp the actual delay selected +for the next recurrence with `Schedule.modifyDelay`. The clamp is +`Duration.min(delay, Duration.seconds(5))`. + +`Schedule.exponential` still outputs the uncapped exponential duration. +`Schedule.modifyDelay` changes the actual sleep used by the schedule. Add +stopping behavior separately with `Schedule.recurs` or `Schedule.during`. + +##### Example + +```ts runnable deterministic +import { Console, Data, Duration, Effect, Schedule } from "effect" + +class ControlPlaneUnavailable extends Data.TaggedError( + "ControlPlaneUnavailable" +)<{ + readonly service: string + readonly attempt: number +}> {} + +let attempts = 0 + +const refreshRoutingTable = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`refresh attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail( + new ControlPlaneUnavailable({ + service: "routing", + attempt: attempts + }) + ) + } + + return "routes refreshed" +}) + +const capAt5Seconds = (delay: Duration.Duration) => Duration.min(delay, Duration.seconds(5)) + +const cappedBackoff = Schedule.exponential("250 millis").pipe( + Schedule.modifyDelay((_, delay) => Effect.succeed(capAt5Seconds(delay))), + Schedule.tapInput((error: ControlPlaneUnavailable) => + Console.log(`retrying ${error.service} after attempt ${error.attempt}`) + ), + Schedule.tapOutput((rawDelay) => + Console.log( + `raw next delay: ${Duration.format(rawDelay)}, capped at: ${Duration.format(capAt5Seconds(rawDelay))}` + ) + ), + Schedule.both(Schedule.recurs(8)) +) + +const program = refreshRoutingTable.pipe( + Effect.retry(cappedBackoff), + Effect.flatMap((message) => Console.log(`result: ${message}`)) +) + +Effect.runPromise(program) +// Output: +// refresh attempt 1 +// retrying routing after attempt 1 +// raw next delay: 250ms, capped at: 250ms +// refresh attempt 2 +// retrying routing after attempt 2 +// raw next delay: 500ms, capped at: 500ms +// refresh attempt 3 +// retrying routing after attempt 3 +// raw next delay: 1s, capped at: 1s +// refresh attempt 4 +// result: routes refreshed +``` + +`Schedule.tapInput` logs the failure that caused a retry. `Schedule.tapOutput` +logs the raw exponential output and the capped value used by the delay +calculation. + +##### Variants + +Use a smaller cap for interactive work. Add `Schedule.during` when the whole +retry window needs an elapsed budget. For fleet-wide retry paths, apply +`Schedule.jittered` before the final cap so randomization does not break the +maximum-delay guarantee. + +##### Notes and caveats + +`Schedule.recurs(8)` means eight retries after the original attempt, not eight +total attempts. + +Capping long tails is an operational visibility tool, not just a latency tweak: +dashboards, logs, alerts, and humans can reason about the next retry without +reading scattered sleeps or hidden counters. + +#### 17.5 Cap delays without losing backoff benefits + +Use this to keep the useful early shape of exponential backoff while preventing +late delays from becoming too long. + +##### Problem + +Backoff should reduce pressure quickly: `250 millis`, `500 millis`, `1 second`, +`2 seconds`, and so on. The same curve can later drift into 16, 32, or 64 second +waits. The policy should say both things: grow while the delay is small, then +stop growing at the cap. + +##### When to use it + +Use it for retry or reconnect loops where short early retries are helpful but +long tail delays are not: control-plane calls, startup probes, worker reconnects, +and idempotent remote operations. It is also useful when the maximum single +delay is part of the operational contract. + +##### When not to use it + +Do not use capped backoff to make permanent failures look transient. Classify +validation errors, authorization failures, malformed requests, and unsafe +non-idempotent writes before `Effect.retry` applies the schedule. + +Avoid it when a fixed cadence is the real requirement. If every retry should +wait exactly 5 seconds, use `Schedule.spaced("5 seconds")`. + +##### Schedule shape + +Build the policy in two steps: + +- start with `Schedule.exponential`, which outputs the computed delay and uses + that delay before the next recurrence +- apply `Schedule.modifyDelay` with `Duration.min` so the delay used by the + schedule is never larger than the cap + +The cap does not flatten the whole policy. With a base of `250 millis` and a +5-second cap, the early delays are still `250 millis`, `500 millis`, `1 second`, +`2 seconds`, and `4 seconds`. Only computed delays above 5 seconds are replaced. + +##### Example + +```ts runnable deterministic +import { Console, Data, Duration, Effect, Schedule } from "effect" + +class RemoteError extends Data.TaggedError("RemoteError")<{ + readonly attempt: number +}> {} + +let attempts = 0 + +const callControlPlane = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail(new RemoteError({ attempt: attempts })) + } + + return "ok" +}) + +const capAt5Seconds = (delay: Duration.Duration) => Duration.min(delay, Duration.seconds(5)) + +const cappedCadence = Schedule.exponential("250 millis").pipe( + Schedule.modifyDelay((_, delay) => Effect.succeed(capAt5Seconds(delay))), + Schedule.tapOutput((rawDelay) => + Console.log( + `raw delay ${Duration.format(rawDelay)} -> capped ${Duration.format(capAt5Seconds(rawDelay))}` + ) + ) +) + +const retryPolicy = cappedCadence.pipe( + Schedule.both(Schedule.recurs(8)) +) + +const program = callControlPlane.pipe( + Effect.retry(retryPolicy), + Effect.flatMap((result) => Console.log(`result: ${result}`)) +) + +Effect.runPromise(program) +// Output: +// attempt 1 +// raw delay 250ms -> capped 250ms +// attempt 2 +// raw delay 500ms -> capped 500ms +// attempt 3 +// raw delay 1s -> capped 1s +// attempt 4 +// result: ok +``` + +Delays below the cap pass through unchanged, so the policy keeps the early +benefit of exponential spacing. Delays above the cap are limited, so the late +retry loop cannot become quieter than the workflow allows. + +##### Variants + +For a gentler curve, pass a smaller factor to `Schedule.exponential`, such as +`Schedule.exponential("250 millis", 1.5)`. The same cap still applies; it just +takes more recurrences to reach it. + +For many instances using the same retry policy, apply `Schedule.jittered` before +the final cap. That spreads retry traffic while preserving the maximum-delay +promise. + +##### Notes and caveats + +`Schedule.modifyDelay` changes the delay used between recurrences; it does not +change the schedule output. In the example, `Schedule.tapOutput` receives the +raw exponential delay and computes the capped value separately for logging. + +`Effect.retry` feeds failures into the schedule. `Effect.repeat` feeds +successful values into the schedule. That distinction matters if you later add +predicates or observation hooks such as `Schedule.tapInput`. + +### 18. Spacing and Throttling + +#### 18.1 At least one request per second + +Use `Schedule` to make a repeat loop's pacing visible instead of hiding sleeps +around request code. + +##### Problem + +A client or worker may need to keep making requests without running in a tight +loop. Reviewers should be able to see whether the one-second rule is a +post-completion gap or a fixed interval boundary, because those shapes behave +differently when requests are slow. + +##### When to use it + +Use this when a single fiber should keep sending requests with a controlled gap +between them. It fits background synchronization, lightweight polling, +heartbeat-style calls, and integrations where a steady request stream is useful +but bursts are not. + +It is especially useful when request duration should contribute to the overall +spacing. If a request takes 300 milliseconds and the schedule is spaced by one +second, the next request starts about one second after the previous request +completed, not 700 milliseconds later. + +##### When not to use it + +Do not use this wording when you really mean a minimum throughput guarantee. +`Schedule.spaced("1 second")` can prevent a loop from running more frequently +than one request plus one gap, but it cannot ensure at least one completed +request per second when requests are slow, blocked, retried elsewhere, or +interrupted. + +Do not use this as a fleet-wide rate limiter. A schedule controls one repeated +effect. Coordinating many fibers, processes, or hosts needs a shared limiter, +queue, semaphore, or service-side quota policy. + +Do not use `Effect.repeat` to retry failed requests. With `Effect.repeat`, a +typed failure from the request stops the repeat. If failures should be retried, +apply a retry policy around the request itself and then repeat the successful +request loop. + +##### Schedule shape + +The basic policy is `Schedule.spaced("1 second")`. It recurs continuously and +contributes a one-second delay to every recurrence decision. With +`Effect.repeat`, the first request runs immediately. After each successful +request, the schedule waits one second before the next request starts. + +The shape is: + +- request 1: run immediately +- if request 1 succeeds: wait one second +- request 2: run again +- if request 2 succeeds: wait one second +- continue until the fiber is interrupted, the request fails, or a bounded + variant stops the schedule + +Use `Schedule.fixed("1 second")` for a different shape: it targets fixed +one-second interval boundaries. If a request takes longer than the interval, +the next run happens immediately, but missed runs do not pile up. + +##### Example + +```ts +import { Console, Effect, Ref, Schedule } from "effect" + +type RequestError = { + readonly _tag: "RequestError" + readonly message: string +} + +const oneSecondAfterEachRequest = Schedule.spaced("1 second").pipe( + Schedule.take(2) +) + +const program = Effect.gen(function*() { + const sent = yield* Ref.make(0) + + const sendRequest: Effect.Effect = Ref.updateAndGet( + sent, + (n) => n + 1 + ).pipe( + Effect.tap((requestNumber) => Console.log(`sent request ${requestNumber}`)), + Effect.flatMap(() => Effect.sleep("25 millis")) + ) + + const finalRecurrence = yield* sendRequest.pipe( + Effect.repeat(oneSecondAfterEachRequest) + ) + + yield* Console.log(`schedule stopped after recurrence ${finalRecurrence}`) +}) + +Effect.runPromise(program) +``` + +`program` sends the first request immediately, then waits one second after each +successful request before the next run. `Schedule.take(2)` keeps the example +finite: one initial run plus two scheduled recurrences. + +The schedule output is the recurrence count. The operational contract is the +delay between successful request runs. + +##### Variants + +Bound the loop when the worker should perform only a limited number of +additional requests. The first request still runs immediately; `Schedule.take(5)` +limits the scheduled recurrences after that first request. + +Use `Schedule.fixed("1 second")` when fixed interval boundaries are the +requirement. It is not the same as "sleep one second after each request"; if a +request runs long, the next request may start immediately. + +##### Notes and caveats + +Be careful with the phrase "at least one request per second". In ordinary rate +limiting language, this recipe is closer to "no more often than one request +plus one one-second gap per loop". It spaces requests; it does not guarantee a +minimum successful request rate. + +`Schedule.spaced("1 second")` delays recurrences; it does not delay the first +request. The first execution of the repeated effect happens immediately. + +`Schedule.fixed("1 second")` and `Schedule.spaced("1 second")` are both real +cadence APIs, but they answer different questions. Use `spaced` for a gap after +work completes. Use `fixed` for fixed interval boundaries. + +`Effect.repeat` feeds successful values into the schedule. Failed requests do +not become schedule inputs; they stop the repeat unless you handle or retry +them before repeating. + +#### 18.2 Process a batch with gaps between items + +Use `Schedule.spaced` when a finite batch should move one item at a time with a +visible pause between successful sends. + +##### Problem + +An import worker may call a partner API, write to a rate-limited database, or +publish messages to a broker. Each item can be processed independently, but a +back-to-back batch can create a short spike in connections, locks, queue depth, +or remote requests. + +##### When to use it + +Use this when the important rule is "after a successful item, wait before +starting the next item." + +`Schedule.spaced(duration)` is the direct fit for dependency pressure caused by +successful work. It waits after the previous item finishes, so the dependency +gets a quiet period before the next item starts. + +This is appropriate for small to moderate batches where sequential processing is +acceptable and the gap is part of the operational contract. + +##### When not to use it + +Do not use this to retry a failed item by itself. With `Effect.repeat`, failures +stop the repeat. If an item should be retried, apply an explicit retry policy +around the item processor before returning success to the batch loop. + +Do not use spacing as the only protection for a heavily shared dependency. Gaps +reduce pressure from one worker, but they do not replace concurrency limits, +rate-limit headers, bulkheads, queue backpressure, or admission control. + +Do not use this when the batch must complete as quickly as possible and the +dependency can safely absorb the burst. In that case a plain sequential +`Effect.forEach` may be clearer. + +##### Schedule shape + +The repeated effect returns the number of items remaining after the item it just +processed. `Schedule.while` sees that successful value as `input`. If more items +remain, the schedule waits and allows the next recurrence. If no items remain, +the repeat stops immediately. + +Use `Schedule.spaced` rather than `Schedule.fixed` when you want the gap to be +measured after each item completes. If an item takes 100 milliseconds to send and +the spacing is 250 milliseconds, the next item starts about 350 milliseconds +after the previous item started. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Ref, Schedule } from "effect" + +type BatchItem = { + readonly id: string + readonly payload: string +} + +type DependencyError = { + readonly _tag: "DependencyError" + readonly itemId: string +} + +const items: ReadonlyArray = [ + { id: "a", payload: "alpha" }, + { id: "b", payload: "bravo" }, + { id: "c", payload: "charlie" } +] + +const gapBetweenItems = Schedule.spaced("50 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input > 0) +) + +const program = Effect.gen(function*() { + const remaining = yield* Ref.make(items) + + const sendToDependency = ( + item: BatchItem + ): Effect.Effect => Console.log(`sent ${item.id}: ${item.payload}`) + + const processNext = Effect.gen(function*() { + const item = yield* Ref.modify(remaining, (items) => + [ + items[0], + items.slice(1) + ] as const) + + if (item === undefined) { + return 0 + } + + yield* sendToDependency(item) + + const left = yield* Ref.get(remaining) + yield* Console.log(`${left.length} item(s) left`) + + return left.length + }) + + const finalRemaining = yield* processNext.pipe( + Effect.repeat(gapBetweenItems) + ) + + yield* Console.log(`batch complete; remaining=${finalRemaining}`) +}) + +Effect.runPromise(program) +// Output: +// sent a: alpha +// 2 item(s) left +// sent b: bravo +// 1 item(s) left +// sent c: charlie +// 0 item(s) left +// batch complete; remaining=0 +``` + +The first item is sent immediately. If more items remain, the schedule waits +before processing the next item. After the last item succeeds, `processNext` +returns `0`, so the schedule stops without adding another gap. The snippet uses +a short gap so it finishes quickly in a scratchpad. + +##### Variants + +Use a longer gap when the downstream dependency shows pressure through rising +latency, lock waits, queue depth, rate-limit responses, or connection pool +exhaustion. + +Use a shorter gap when each item is cheap and the dependency has enough spare +capacity. Keep the chosen duration visible in a named schedule so operators can +tune it without reverse-engineering a loop. + +If many workers may start batches at the same time, add `Schedule.jittered` +after choosing the base spacing. Jitter reduces synchronized pressure across +workers, but it does not bound total throughput by itself. Pair it with worker +concurrency limits or a dependency rate limiter when the dependency has a hard +quota. + +##### Notes and caveats + +The schedule does not delay the first item. It controls only recurrences after a +successful item has been processed. + +`Effect.repeat` feeds successful values into the schedule. In this recipe, the +successful value is the remaining item count, and `Schedule.while` uses it to +decide whether another recurrence is needed. + +If `sendToDependency` fails, the batch stops with that failure. Add item-level +classification and retry separately if transient failures should be retried. + +`Schedule.spaced` measures the delay after the previous item completes. That is +usually what you want for dependency pressure, because slow items naturally +reduce the start rate of later items. + +#### 18.3 Avoid hammering an external API + +Use a schedule to make retry spacing explicit for external APIs. The first call +is immediate, and only follow-up attempts after a failure are paced. + +##### Problem + +You call a third-party API from a worker. The request is replay-safe because it +uses an idempotency key, but the service sometimes responds with a timeout, a +short rate-limit window, or a transient server error. Reviewers should be able +to see: + +- how quickly retries start +- how retries spread out over time +- how many extra requests the policy can create +- which failures are allowed to retry +- why the request is safe to replay + +##### When to use it + +Use this recipe when a remote call can be retried but should leave breathing +room between attempts. Typical examples are fetching a generated report, +submitting an idempotent event, refreshing data from a vendor API, or retrying a +temporary `429` from a service with a documented quota. + +It is a good fit when retry safety is part of the API contract. The schedule can +limit pressure, but the request still needs to be replayable: use an +idempotency key, a deduplication token, a natural resource identifier, or a +read-only operation. + +##### When not to use it + +Do not use retry spacing to make unsafe side effects safe. Retrying +`POST /payments` or `POST /orders` can duplicate work unless the external API +provides idempotency or another deduplication mechanism. + +Do not retry permanent failures such as invalid input, missing credentials, +forbidden access, or a resource that does not exist. Classify those errors +before the schedule is allowed to recur. + +Do not treat `Schedule` as a distributed rate limiter. A schedule spaces one +program's recurrences. If many processes share the same vendor quota, combine +this retry policy with a real rate limiter or vendor-provided `Retry-After` +handling. + +##### Schedule shape + +Start with the delay shape, then add guardrails. An exponential schedule +starting at 250 milliseconds grows by the default factor of `2`: about `250ms`, +`500ms`, `1s`, `2s`, and so on. `Schedule.jittered` randomly adjusts each +recurrence delay between `80%` and `120%` of the computed delay, which helps +avoid synchronized retries when many workers fail at the same time. + +`Schedule.recurs(5)` bounds the extra requests. `Schedule.during("30 seconds")` +bounds the elapsed retry window when combined with `Schedule.both`. `both` gives +intersection semantics: the combined policy continues only while both component +schedules continue, and it uses the maximum of their delays. + +The code below uses shorter durations so it can be pasted into a scratchpad and +finish quickly. + +##### Example + +```ts +import { Console, Data, Effect, Random, Ref, Schedule } from "effect" + +class VendorApiError extends Data.TaggedError("VendorApiError")<{ + readonly status: number + readonly message: string +}> {} + +interface Enrichment { + readonly companyId: string + readonly riskScore: number +} + +const isRetryableVendorFailure = (error: VendorApiError) => + error.status === 408 || + error.status === 429 || + error.status >= 500 + +const vendorRetryPolicy = Schedule.exponential("30 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.jittered, + Schedule.both(Schedule.recurs(5)), + Schedule.both(Schedule.during("1 second")), + Schedule.while(({ input }) => isRetryableVendorFailure(input)) +) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + + const enrichCompany = (request: { + readonly companyId: string + readonly idempotencyKey: string + }): Effect.Effect => + Effect.gen(function*() { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log( + `vendor attempt ${attempt} with key ${request.idempotencyKey}` + ) + + if (attempt === 1) { + return yield* Effect.fail( + new VendorApiError({ status: 429, message: "slow down" }) + ) + } + + if (attempt === 2) { + return yield* Effect.fail( + new VendorApiError({ status: 503, message: "temporary outage" }) + ) + } + + return { companyId: request.companyId, riskScore: 42 } + }) + + const enrichment = yield* enrichCompany({ + companyId: "company_123", + idempotencyKey: "enrich-company_123" + }).pipe( + Effect.retry(vendorRetryPolicy), + Random.withSeed("vendor-retry-demo") + ) + + yield* Console.log(`risk score: ${enrichment.riskScore}`) +}) + +Effect.runPromise(program) +``` + +The first `enrichCompany` call happens immediately. If it fails with a retryable +`VendorApiError`, `Effect.retry` feeds that failure into the schedule. The +schedule waits for the jittered exponential delay and then allows another +attempt. If the error is not retryable, the retry count is exhausted, or the +elapsed budget is exceeded, the original failure is returned. + +##### Variants + +For a strict published quota, choose a base delay that respects the quota even +under retries. If the API allows one request per second per tenant, a +`Schedule.spaced("1 second")` policy may be clearer than exponential backoff +because it states the minimum gap directly. + +For a bursty worker fleet, keep jitter enabled and consider a larger starting +delay. Jitter spreads retries from identical clients, but it does not coordinate +quota across processes. + +If the vendor returns `Retry-After`, prefer honoring that response when it is +available. A local schedule is a fallback policy; a server-provided delay is +usually the more accurate rate-limit signal. + +##### Notes and caveats + +`Effect.retry` is failure-driven. It retries only after the effect fails, and it +passes the failure to the schedule as `input`. That is why `Schedule.while` can +inspect `VendorApiError` and stop on non-retryable statuses. + +`Schedule.exponential` and `Schedule.spaced` are unbounded by themselves. Always +combine them with a retry count, elapsed budget, domain predicate, or enclosing +lifetime when calling an external API. + +Spacing protects the dependency from immediate retry bursts, but it does not +guarantee global compliance with a vendor quota. Use a shared rate limiter when +the limit applies across workers, tenants, or service instances. + +Retry safety is separate from timing. Before adding a schedule, make sure the +operation is read-only or replay-safe through idempotency, deduplication, or a +vendor contract that explicitly permits retrying. + +#### 18.4 Smooth demand over time + +Use `Schedule.spaced` and, when needed, `Schedule.jittered` to turn repeated +work into a paced stream with visible timing rules. + +##### Problem + +Queue draining, cache warming, search indexing, and remote API calls can create +uneven pressure when they run as quickly as possible: idle time, a burst of +requests, then more idle time. + +The schedule should make each worker's pace explicit, and a fleet should avoid +synchronized requests when instances share the same configuration. + +##### When to use it + +Use this recipe for background loops where each repetition is safe and useful on +its own, but bursty demand would hurt a downstream service, database, queue, or +cache. + +It is especially useful when several process instances run the same loop. A +shared one-second spacing gives every instance the same average pace, while +jitter gives each instance a slightly different actual delay on each recurrence. + +##### When not to use it + +Do not use spacing and jitter as a substitute for real concurrency limits, queue +backpressure, rate-limit handling, or overload protection. A schedule controls +when the next repetition is attempted; it does not know how much work is waiting +or how much capacity the downstream system currently has. + +Do not add jitter when exact wall-clock cadence matters, such as emitting a +sample exactly on a reporting boundary. In that case, choose a precise cadence +deliberately and accept that it may align across workers. + +##### Schedule shape + +`Schedule.spaced("1 second")` waits one second after each successful repetition +before the next repetition is started. This differs from `Schedule.fixed`, which +tries to maintain a wall-clock interval and may run immediately if the previous +action took longer than the interval. + +`Schedule.jittered` adjusts each computed delay between `80%` and `120%` of the +original delay. Applied to a one-second spaced schedule, each sleep is randomly +chosen between 800 milliseconds and 1.2 seconds. The average pace stays close to +the base spacing, but instances no longer line up perfectly. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Random, Ref, Schedule } from "effect" + +type WorkItem = { + readonly id: string +} + +type WorkerError = { + readonly _tag: "WorkerError" +} + +const initialItems: ReadonlyArray = [ + { id: "job-1" }, + { id: "job-2" }, + { id: "job-3" }, + { id: "job-4" } +] + +const smoothedDemand = Schedule.spaced("40 millis").pipe( + Schedule.jittered, + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input > 0) +) + +const program = Effect.gen(function*() { + const queue = yield* Ref.make(initialItems) + + const processNextItem: Effect.Effect = Effect.gen( + function*() { + const item = yield* Ref.modify(queue, (items) => + [ + items[0], + items.slice(1) + ] as const) + + if (item === undefined) { + return 0 + } + + yield* Console.log(`processed ${item.id}`) + + const remaining = yield* Ref.get(queue) + return remaining.length + } + ) + + const remaining = yield* processNextItem.pipe( + Effect.repeat(smoothedDemand), + Random.withSeed("smoothed-demand-demo") + ) + + yield* Console.log(`queue drained; remaining=${remaining}`) +}) + +Effect.runPromise(program) +// Output: +// processed job-1 +// processed job-2 +// processed job-3 +// processed job-4 +// queue drained; remaining=0 +``` + +The first `processNextItem` run happens immediately. The schedule controls only +the follow-up repetitions. Each successful run is followed by a jittered delay +around the base spacing, and `Schedule.while` stops the loop when the worker +reports that no items remain. The snippet uses millisecond-scale spacing so it +finishes quickly in a scratchpad. + +##### Variants + +For a single worker where only local pacing matters, remove `Schedule.jittered` +and keep the spacing deterministic. + +For a larger fleet, keep the jitter even when the base interval is short. The +spacing controls average demand; the jitter reduces alignment between instances. + +For long-running workers, make the lifecycle boundary explicit in the fiber, +queue, or service that owns the loop. Keep the spacing policy named so operators +can still see the intended load profile. + +##### Notes and caveats + +`Effect.repeat` feeds successful values into the schedule. If +`processNextItem` fails, the repeat stops unless you handle or retry that error +separately. + +`Schedule.spaced` recurs indefinitely by itself. Pair it with a stop condition +when the loop is meant to finish, or make the owning process lifetime explicit +when the loop is meant to run continuously. + +`Schedule.jittered` changes delay timing only. It does not change the work +effect, the success value, or the error channel. Keep randomness in the schedule +so readers can understand the demand-shaping contract from one value. + +#### 18.5 Drain a queue slowly + +A queue drain is repeat work, not retry work. Run one item, decide whether more +work remains, and let a schedule add the pause before the next item. + +##### Problem + +A local queue already contains work. Processing everything in a tight loop can +burst against a database, API, or shared worker pool. The drain should make +steady progress, stop when the queue is empty, and cap how much one invocation +can do. + +`Queue.take` waits when the queue is empty, so the drain step should check for +available work before taking. The successful step result can then tell +`Effect.repeat` whether another scheduled pass is useful. + +##### When to use it + +Use this for local buffers, outbox dispatchers, maintenance queues, and +reprocessing backlogs where empty means "this drain pass is done." The repeated +effect should process one item or one small batch and return whether more work +is likely. + +##### When not to use it + +Do not use this as a long-lived consumer that should block for future messages. +A normal consumer loop can call `Queue.take` and let the queue provide +backpressure. + +Do not use the drain schedule to recover from item-processing failures. +`Effect.repeat` schedules successful values. If processing can fail +transiently, retry that item-processing effect separately. + +Do not treat `Queue.size` as a transactional reservation in a multi-consumer +queue. It is fine for a single drain worker deciding whether to keep going, but +another consumer can change the size at any time. + +##### Schedule shape + +Use `Schedule.spaced` for the pause between successful drain steps and combine +it with a recurrence limit such as `Schedule.recurs(99)`. Put the empty-queue +stop condition in `Effect.repeat({ while })`, where it can inspect the +`DrainStep` returned by the effect. + +The first item is processed immediately. The schedule controls only the +follow-up drain steps. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Queue, Schedule } from "effect" + +type WorkItem = { + readonly id: number + readonly payload: string +} + +type DrainStep = + | { readonly _tag: "Processed"; readonly item: WorkItem; readonly remaining: number } + | { readonly _tag: "Drained" } + +const processItem = (item: WorkItem) => Console.log(`processed item ${item.id}: ${item.payload}`) + +const drainOneAvailableItem = Effect.fnUntraced(function*(queue: Queue.Queue) { + const queued = yield* Queue.size(queue) + + if (queued === 0) { + yield* Console.log("queue is empty") + return { _tag: "Drained" } as const + } + + const item = yield* Queue.take(queue) + yield* processItem(item) + + const remaining = yield* Queue.size(queue) + yield* Console.log(`${remaining} item(s) remain`) + + return { _tag: "Processed", item, remaining } as const +}) + +const slowDrainPolicy = Schedule.spaced("10 millis").pipe( + Schedule.both(Schedule.recurs(9)) +) + +const shouldContinue = (step: DrainStep) => step._tag === "Processed" && step.remaining > 0 + +const program = Effect.gen(function*() { + const queue = yield* Queue.unbounded() + yield* Queue.offerAll(queue, [ + { id: 1, payload: "refresh-search-index" }, + { id: 2, payload: "publish-outbox-event" }, + { id: 3, payload: "expire-cache-entry" } + ]) + + yield* drainOneAvailableItem(queue).pipe( + Effect.repeat({ + schedule: slowDrainPolicy, + while: shouldContinue + }) + ) + + yield* Console.log("drain pass finished") +}) + +Effect.runPromise(program) +// Output: +// processed item 1: refresh-search-index +// 2 item(s) remain +// processed item 2: publish-outbox-event +// 1 item(s) remain +// processed item 3: expire-cache-entry +// 0 item(s) remain +// drain pass finished +``` + +The demo uses `10 millis` so it terminates quickly. In production, choose a gap +from the dependency you are protecting, such as a database write budget or API +quota. + +##### Variants + +For batch drains, process a small batch and return the remaining count. Keep +the same schedule shape: a spacing policy, a count limit, and a stop condition +based on the successful drain result. + +For shared queues, move reservation semantics into the queue or database claim +operation. The schedule can pace successful work, but it cannot make `size` +stable across workers. + +##### Notes and caveats + +`Effect.repeat` feeds successful `DrainStep` values into the repeat decision. +Failures from `processItem` stop the drain unless the processing effect has its +own retry policy. + +`Schedule.spaced` waits after a successful iteration completes. `Schedule.fixed` +is different: it follows interval boundaries and may run again immediately if a +previous iteration took longer than the interval. + +### 19. Rate Limits and User-Facing Effects + +#### 19.1 Send emails with controlled spacing + +Email delivery is a user-visible write. Retry timing should be explicit, +bounded, and limited to failures that are safe to retry. + +##### Problem + +An email provider can fail with timeouts, temporary unavailability, or +rate-limit responses. Immediate retries can exceed quotas, trigger throttling, +or create duplicate-looking messages when the provider accepted the first +request but the response was lost. + +The retry policy should show the delay between attempts, the retry count, and +the failure types that are retryable. + +##### When to use it + +Use this for transactional or notification email where retrying can help: +welcome emails, password resets, invoices, account alerts, and queued +notifications. + +It works best when the provider supports a stable idempotency key, message key, +or client reference. That key should identify the logical email and be reused +for every attempt. + +##### When not to use it + +Do not retry invalid recipients, malformed content, suppressed addresses, +authorization failures, or provider rejections that are clearly permanent. + +Do not treat `Schedule.spaced` as an account-wide rate limiter. It spaces this +effect's attempts; queue concurrency and shared quotas still need their own +controls. + +##### Schedule shape + +Use a small bounded retry policy. `Schedule.spaced("30 seconds")` leaves a +fixed gap between failed attempts, and `Schedule.recurs(3)` allows at most +three retries after the original send. `Effect.retry({ schedule, while })` +applies the schedule only to failures accepted by the predicate. + +The first provider call is not delayed. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class EmailDeliveryError extends Data.TaggedError("EmailDeliveryError")<{ + readonly reason: + | "Timeout" + | "ProviderUnavailable" + | "RateLimited" + | "InvalidRecipient" + | "RejectedContent" +}> {} + +interface EmailMessage { + readonly to: string + readonly subject: string + readonly bodyText: string + readonly idempotencyKey: string +} + +interface ProviderMessageId { + readonly value: string +} + +let attempts = 0 + +const sendViaProvider = (message: EmailMessage) => + Effect.gen(function*() { + attempts += 1 + yield* Console.log(`email attempt ${attempts} using key ${message.idempotencyKey}`) + + if (attempts === 1) { + return yield* Effect.fail(new EmailDeliveryError({ reason: "Timeout" })) + } + + return { value: `provider-${message.idempotencyKey}` } satisfies ProviderMessageId + }) + +const emailRetrySpacing = Schedule.spaced("20 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +const isRetryableEmailFailure = (error: EmailDeliveryError): boolean => { + switch (error.reason) { + case "Timeout": + case "ProviderUnavailable": + case "RateLimited": + return true + case "InvalidRecipient": + case "RejectedContent": + return false + } +} + +const sendEmailWithControlledSpacing = (message: EmailMessage) => + sendViaProvider(message).pipe( + Effect.retry({ + schedule: emailRetrySpacing, + while: isRetryableEmailFailure + }) + ) + +const program = sendEmailWithControlledSpacing({ + to: "user@example.com", + subject: "Your report is ready", + bodyText: "Open the dashboard to view it.", + idempotencyKey: "email:report-ready:user-123" +}).pipe( + Effect.tap((receipt) => Console.log(`accepted as ${receipt.value}`)) +) + +Effect.runPromise(program) +// Output: +// email attempt 1 using key email:report-ready:user-123 +// email attempt 2 using key email:report-ready:user-123 +// accepted as provider-email:report-ready:user-123 +``` + +The demo uses `20 millis` so it finishes quickly. In production, choose spacing +from provider quota and user experience. The idempotency key belongs to the +logical email, not to a single HTTP attempt. + +##### Variants + +Interactive flows usually need fewer retries and shorter spacing so the caller +gets a timely answer. Outbox workers can use longer spacing and more attempts +because the user is no longer blocked on the request. + +When many workers may retry the same kind of email, add `Schedule.jittered` +after the base spacing. Use jitter for fleet behavior, not when a provider +requires precise minimum spacing. + +##### Notes and caveats + +`Effect.retry` feeds failures into the schedule. Permanent failures bypass the +schedule when the `while` predicate returns `false`. + +`Schedule.recurs(3)` means three retries after the original attempt, not three +total provider calls. + +Spacing reduces burstiness; it does not make delivery idempotent. Duplicate +prevention comes from the provider contract and from reusing the same stable +key. + +#### 19.2 Respect provider quotas + +Provider quotas make retry timing part of the integration contract. Use fixed +spacing when the rule is a minimum gap between attempts. + +##### Problem + +A provider enforces a documented quota, such as one request per second. Quick +retries after a timeout or `429 Too Many Requests` can violate that quota even +when the code is local and small. + +The policy should show the minimum retry spacing, the maximum number of extra +provider calls, and the failures that count as retryable quota or availability +signals. + +##### When to use it + +Use this when one client, worker, or user-facing path must avoid bursty retries +against a quota-protected provider. It fits idempotent calls such as sending a +notification with a deduplication key, refreshing customer metadata, checking +delivery status, or submitting a provider request with a documented retry +contract. + +`Schedule.spaced` is clearest when the provider quota is a steady rate. + +##### When not to use it + +Do not use a local schedule as a fleet-wide rate limiter. A one-second +`Schedule.spaced` policy spaces one retrying effect, not every fiber, process, +tenant, or deployment sharing the account. + +Do not retry permanent failures. Classify errors before the schedule is allowed +to recur. + +If the response includes `Retry-After` or a quota reset timestamp, prefer that +provider guidance for the rate-limit case and keep fixed spacing as a fallback. + +##### Schedule shape + +Combine `Schedule.spaced` with `Schedule.recurs`, then pass a retry predicate to +`Effect.retry`. The schedule controls when another attempt may happen; the +predicate controls whether a failure is allowed to use the schedule. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class ProviderError extends Data.TaggedError("ProviderError")<{ + readonly status: number + readonly reason: string +}> {} + +interface DeliveryReceipt { + readonly messageId: string + readonly accepted: boolean +} + +type ProviderRequest = { + readonly tenantId: string + readonly messageId: string + readonly idempotencyKey: string +} + +let attempts = 0 + +const sendProviderMessage = (request: ProviderRequest) => + Effect.gen(function*() { + attempts += 1 + yield* Console.log(`provider attempt ${attempts} for ${request.messageId}`) + + if (attempts === 1) { + return yield* Effect.fail(new ProviderError({ status: 429, reason: "rate limited" })) + } + if (attempts === 2) { + return yield* Effect.fail(new ProviderError({ status: 503, reason: "unavailable" })) + } + + return { messageId: request.messageId, accepted: true } satisfies DeliveryReceipt + }) + +const isRetryableProviderError = (error: ProviderError) => + error.status === 408 || + error.status === 429 || + error.status === 500 || + error.status === 502 || + error.status === 503 || + error.status === 504 + +const providerQuotaPolicy = Schedule.spaced("20 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +const program = sendProviderMessage({ + tenantId: "tenant-123", + messageId: "message-456", + idempotencyKey: "tenant-123:message-456" +}).pipe( + Effect.retry({ + schedule: providerQuotaPolicy, + while: isRetryableProviderError + }), + Effect.tap((receipt) => Console.log(`accepted: ${receipt.accepted}`)) +) + +Effect.runPromise(program) +// Output: +// provider attempt 1 for message-456 +// provider attempt 2 for message-456 +// provider attempt 3 for message-456 +// accepted: true +``` + +The original provider call is immediate. Retryable failures are spaced by the +schedule and stop after the retry count is exhausted. + +##### Variants + +For stricter quotas, choose spacing from the published limit. Twelve requests +per minute implies at least five seconds between attempts for one worker. + +For user-facing flows, reduce retry count or add an elapsed budget. For +background work, longer intervals and more attempts may be acceptable when the +provider contract permits them. + +For many workers sharing one provider account, keep this as the local retry +shape and add shared admission control around the provider call. + +##### Notes and caveats + +`Effect.retry` is failure-driven. Successful provider responses end the retry +loop immediately; typed failures are passed to the retry policy. + +`Schedule.spaced` is unbounded by itself. Combine it with a retry limit, elapsed +budget, domain predicate, or enclosing workflow lifetime when calling a +third-party API. + +A `429` can be retryable when quota will refill soon. A hard quota exhaustion, +invalid API key, or forbidden tenant usually should not retry. + +#### 19.3 Space calls to a third-party API + +Third-party clients usually need two policies: a steady cadence for normal +traffic and a smaller retry policy around each individual call. + +##### Problem + +You need to send requests to a provider without starting the next request as +soon as the previous one finishes. The rule should be easy to review: send one +item, wait, then send the next item. + +A single request may still need retries for transient failures such as timeouts, +temporary unavailability, or retryable rate-limit responses. Keep that retry +policy separate from the outer spacing policy so provider quota behavior is not +hidden inside call-level failure handling. + +Use `Effect.repeat` with `Schedule.spaced(duration)` for the worker cadence, and +use `Effect.retry` around the one provider call when retrying is safe. + +##### When to use it + +Use this for one worker, fiber, or shard that should avoid back-to-back calls to +a provider. It fits ingestion jobs, webhook delivery, enrichment pipelines, and +partner synchronization where a provider publishes a rough quota such as one +request per second. + +Use it when "wait after each completed call" is the intended behavior. +`Schedule.spaced("1 second")` waits after the previous run finishes; slow API +responses naturally reduce the total request rate. + +##### When not to use it + +Do not treat a local schedule as a global rate limiter. If many processes, +hosts, tenants, or shards share one provider quota, coordinate that quota with +shared admission control. + +Do not retry unsafe writes unless the API supports idempotency keys or another +deduplication mechanism. A timeout can mean the provider accepted the request +but the client did not receive the response. + +Do not use guessed spacing when the provider gives an exact `Retry-After` value +that must be followed. Classify that response and derive the retry delay from +the provider signal. + +##### Schedule shape + +The outer policy is `Schedule.spaced(duration)`. With `Effect.repeat`, the first +effect run starts immediately. After a successful run, the schedule waits before +allowing the next recurrence. The time spent inside the provider call is not +subtracted from the delay; this is spacing, not a fixed wall-clock rate. + +Add `Schedule.recurs(n)` when the worker should make only `n` additional +successful recurrences. For a long-lived supervised worker, fiber lifetime or a +queue shutdown signal may be the stop condition instead. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Ref, Schedule } from "effect" + +type PartnerEvent = { + readonly idempotencyKey: string + readonly payload: string +} + +type PartnerError = + | { readonly _tag: "Timeout" } + | { readonly _tag: "Unavailable" } + | { readonly _tag: "RateLimited" } + | { readonly _tag: "Rejected"; readonly reason: string } + +const events: ReadonlyArray = [ + { idempotencyKey: "event-1", payload: "alpha" }, + { idempotencyKey: "event-2", payload: "bravo" }, + { idempotencyKey: "event-3", payload: "charlie" } +] + +const nextEvent = Effect.fnUntraced(function*(cursor: Ref.Ref) { + const index = yield* Ref.updateAndGet(cursor, (n) => n + 1) + const event = events[index - 1] + + if (event === undefined) { + return yield* Effect.fail({ _tag: "NoMoreEvents" } as const) + } + + yield* Console.log(`next: ${event.idempotencyKey}`) + return event +}) + +const postToPartner = Effect.fnUntraced(function*( + calls: Ref.Ref, + event: PartnerEvent +) { + const callNumber = yield* Ref.updateAndGet(calls, (n) => n + 1) + yield* Console.log(`provider call ${callNumber}: ${event.payload}`) + + if (callNumber === 1) { + return yield* Effect.fail({ _tag: "Unavailable" } as const) + } + + return { acceptedId: `accepted-${event.idempotencyKey}` } +}) + +const isRetryablePartnerError = (error: PartnerError): boolean => error._tag !== "Rejected" + +const retryTransientCallFailure = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.recurs(2)) +) + +const sendOneEvent = Effect.fnUntraced(function*( + cursor: Ref.Ref, + calls: Ref.Ref +) { + const event = yield* nextEvent(cursor) + const response = yield* postToPartner(calls, event).pipe( + Effect.retry({ + schedule: retryTransientCallFailure, + while: isRetryablePartnerError + }) + ) + yield* Console.log(`sent: ${response.acceptedId}`) +}) + +const program = Effect.gen(function*() { + const cursor = yield* Ref.make(0) + const calls = yield* Ref.make(0) + + yield* sendOneEvent(cursor, calls).pipe( + Effect.repeat( + Schedule.spaced("25 millis").pipe( + Schedule.both(Schedule.recurs(2)) + ) + ) + ) + + yield* Console.log("done") +}) + +Effect.runPromise(program) +// Output: +// next: event-1 +// provider call 1: alpha +// provider call 2: alpha +// sent: accepted-event-1 +// next: event-2 +// provider call 3: bravo +// sent: accepted-event-2 +// next: event-3 +// provider call 4: charlie +// sent: accepted-event-3 +// done +``` + +The worker sends the first event immediately. The first provider call fails +once, so `Effect.retry` retries that same event with short exponential spacing. +Only after the call succeeds does the outer `Schedule.spaced` policy wait before +the worker asks for the next event. + +##### Variants + +Add jitter when many local workers use the same cadence and exact spacing is not +required. `Schedule.jittered` adjusts each delay between 80% and 120% of the +computed delay, which helps workers avoid moving together. + +For a batch job, combine `Schedule.spaced(duration)` with `Schedule.recurs(n)`. +For a stricter provider quota, increase the spacing. For a quota shared across +workers, use a real rate limiter instead of trying to encode fleet-wide quota +accounting in each local repeat schedule. + +##### Notes and caveats + +`Schedule.spaced` delays recurrences; it does not delay the first call. If the +first call must wait too, sleep or acquire a permit before entering the repeat. + +`Effect.repeat` feeds successful values into its schedule. `Effect.retry` feeds +typed failures into its schedule. Keeping those schedules separate makes clear +which policy protects normal traffic and which policy handles transient call +failure. + +Classify non-retryable provider responses before retrying. Authentication +errors, validation failures, permanent rejection, and non-idempotent duplicate +risks should fail fast or move the event to a dead-letter path. + +#### 19.4 Slow down after a 429 response + +HTTP `429 Too Many Requests` is a pacing signal. Treat it differently from +ordinary transient failures so retry timing can follow provider guidance. + +##### Problem + +An HTTP API sometimes returns `429`. If the response includes a retry-after +signal, the next attempt should honor it. If the signal is missing, the client +should still wait for a conservative fallback delay. Other failures should not +silently inherit the same policy because they have different operational +meaning. + +##### When to use it + +Use this when the server explicitly says the client is rate limited. Common +sources are a `Retry-After` header, a provider-specific reset header, or a +decoded response field that says when quota should be available again. + +This is a good fit for idempotent calls, background sync jobs, polling workers, +and queued writes where waiting is better than turning a temporary quota limit +into a hard failure. + +##### When not to use it + +Do not use this as a generic HTTP retry policy. A `500` or `503` usually means +the service is unhealthy or overloaded; exponential backoff with jitter and a +short budget is usually a better fit. + +Do not retry unsafe non-idempotent requests unless the protocol gives you an +idempotency key or another deduplication guarantee. Slowing down prevents bursts; +it does not make repeated writes safe. + +##### Schedule shape + +Build the policy around the typed error value: + +- classify retryable errors before scheduling +- use `Schedule.identity()` so the schedule output is the latest error +- use `Schedule.modifyDelay` to choose the next delay from that error +- cap retries with `Schedule.recurs(n)` + +Normalize provider headers into a `Duration` before constructing the typed +`RateLimited` error. The schedule should consume domain data, not parse raw HTTP +headers. + +##### Example + +```ts runnable deterministic +import { Console, Duration, Effect, Ref, Schedule } from "effect" + +type ApiError = + | { + readonly _tag: "RateLimited" + readonly retryAfter: Duration.Duration | undefined + } + | { + readonly _tag: "ServerUnavailable" + } + +const fallback429Delay = Duration.millis(40) + +const retryAfter = (error: ApiError): Duration.Duration => + error._tag === "RateLimited" && error.retryAfter !== undefined + ? error.retryAfter + : fallback429Delay + +const isRateLimited = (error: ApiError): boolean => error._tag === "RateLimited" + +const rateLimitPolicy = Schedule.identity().pipe( + Schedule.while(({ input }) => input._tag === "RateLimited"), + Schedule.modifyDelay((error) => { + const delay = retryAfter(error) + return Console.log(`429 delay: ${Duration.toMillis(delay)}ms`).pipe( + Effect.as(delay) + ) + }), + Schedule.both(Schedule.recurs(4)) +) + +const callApi = Effect.fnUntraced(function*(attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`HTTP attempt ${attempt}`) + + if (attempt === 1) { + return yield* Effect.fail( + { + _tag: "RateLimited", + retryAfter: Duration.millis(25) + } as const + ) + } + + if (attempt === 2) { + return yield* Effect.fail( + { + _tag: "RateLimited", + retryAfter: undefined + } as const + ) + } + + return { body: "ok" } +}) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const response = yield* callApi(attempts).pipe( + Effect.retry({ + schedule: rateLimitPolicy, + while: isRateLimited + }) + ) + yield* Console.log(`response: ${response.body}`) +}) + +Effect.runPromise(program) +// Output: +// HTTP attempt 1 +// 429 delay: 25ms +// HTTP attempt 2 +// 429 delay: 40ms +// HTTP attempt 3 +// response: ok +``` + +The first retry uses the provider's 25 millisecond signal. The second retry uses +the fallback because the simulated response omits `retryAfter`. In production, +use durations that match the provider contract rather than documentation-sized +delays. + +##### Variants + +If the provider gives an absolute reset time, convert it into a duration at the +HTTP boundary and store that duration on the `RateLimited` error. + +If many workers share the same credential, coordinate through a shared limiter. +Only jitter fallback delays when doing so cannot retry before a required +provider minimum. + +If the request is user-facing, combine the retry count with a short elapsed-time +budget. Background jobs can usually afford longer spacing than foreground +requests. + +##### Notes and caveats + +The first call is not delayed. The schedule controls follow-up attempts after +`callApi` fails. + +The fallback delay is part of the contract. Without it, a missing retry-after +signal can become a burst of immediate retries, which is what a rate-limit +policy is meant to prevent. + +#### 19.5 Coordinate retry and rate-limit handling + +Retries and rate limits answer different questions. Classification decides +whether another attempt is allowed; the schedule decides when that attempt may +happen. + +##### Problem + +An external API can fail in several ways: + +- `RateLimited`: retryable, but only after the provider's requested delay +- `ServiceUnavailable`: retryable with ordinary backoff +- `BadRequest`: not retryable; the request must be fixed + +The retry policy should make both decisions explicit. `BadRequest` should not +enter the retry schedule. `RateLimited` should wait at least as long as the +provider asked. `ServiceUnavailable` can use normal backoff. + +##### Recommended policy + +Classify first with `Effect.retry({ while })`. Then use a schedule that can +observe the typed retry input. `Schedule.identity()` exposes the +current failure as the schedule output; `Schedule.modifyDelay` can choose a +delay that matches that failure. + +##### Example + +```ts runnable deterministic +import { Console, Duration, Effect, Ref, Schedule } from "effect" + +type ApiError = + | { + readonly _tag: "RateLimited" + readonly retryAfter: Duration.Duration + } + | { + readonly _tag: "ServiceUnavailable" + } + | { + readonly _tag: "BadRequest" + readonly reason: string + } + +const isRetryable = (error: ApiError): boolean => error._tag === "RateLimited" || error._tag === "ServiceUnavailable" + +const retryPolicy = Schedule.identity().pipe( + Schedule.both(Schedule.exponential("10 millis")), + Schedule.modifyDelay(([error], computedDelay) => { + const delay = error._tag === "RateLimited" + ? Duration.max(computedDelay, error.retryAfter) + : computedDelay + + return Console.log( + `delay for ${error._tag}: ${Duration.toMillis(delay)}ms` + ).pipe(Effect.as(delay)) + }), + Schedule.both(Schedule.recurs(5)) +) + +const callProvider = Effect.fnUntraced(function*(attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`provider attempt ${attempt}`) + + if (attempt === 1) { + return yield* Effect.fail({ _tag: "ServiceUnavailable" } as const) + } + + if (attempt === 2) { + return yield* Effect.fail( + { + _tag: "RateLimited", + retryAfter: Duration.millis(35) + } as const + ) + } + + return "provider-ok" +}) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const result = yield* callProvider(attempts).pipe( + Effect.retry({ + schedule: retryPolicy, + while: isRetryable + }) + ) + yield* Console.log(`result: ${result}`) +}) + +Effect.runPromise(program) +// Output: +// provider attempt 1 +// delay for ServiceUnavailable: 10ms +// provider attempt 2 +// delay for RateLimited: 35ms +// provider attempt 3 +// result: provider-ok +``` + +This policy allows at most five retries after the original call. Ordinary +service unavailability follows the exponential delay. Rate limits use the larger +of the exponential delay and the provider's `retryAfter` value, so the client +never retries earlier than the rate-limit response requested. + +##### Why the pieces are separate + +The `while` predicate is the classification boundary. It says which typed errors +are safe to retry. Permanent failures, validation failures, authorization +failures, and unsafe write failures should be rejected there. + +The `Schedule` is the timing boundary. It says how retryable failures are paced +after classification has allowed them. Because retry schedules receive failures +as input, timing can still distinguish a rate-limit response from a server +unavailable response. + +`Schedule.both` combines the typed input schedule with exponential backoff. The +combined schedule recurs only while both sides recur, and it uses the maximum of +their delays. `Schedule.recurs(5)` adds a hard retry count. + +##### Variants + +If provider guidance must be followed exactly, use a schedule whose base delay +does not add extra backoff for `RateLimited` errors. If many clients may retry +together, add jitter only after deciding whether the provider contract allows +it. + +If the operation is user-facing, combine the retry count with a time budget such +as `Schedule.during("10 seconds")` so callers get a bounded response. Background +workers can use longer budgets with clear logging around the rate-limited path. + +##### Notes and caveats + +`Effect.retry` schedules typed failures from the error channel. It does not turn +defects or interruptions into retryable errors. + +The first call is not delayed. The schedule controls waits between retry +attempts after a failure. + +`Schedule.modifyDelay` replaces the delay chosen by the schedule. Use it when +the rate-limit delay should be compared with, or override, computed backoff. Use +`Schedule.addDelay` when provider delay should be added on top of an existing +delay. + +Retries do not make side effects safe. For writes, classification must account +for idempotency keys, deduplication, or another domain guarantee before any +schedule is applied. + +### 20. Jitter Concepts and Tradeoffs + +#### 20.1 Thundering herds + +Use jitter when many clients, workers, service instances, or fibers might +otherwise retry or poll on the same cadence. + +##### Problem + +A thundering herd is a burst created when many actors react to the same event at +the same time. Deploys, restarts, cache expiry, outage recovery, rate limits, and +shared transient errors can all synchronize clients. A fixed delay that is mild +for one caller can become a sharp load wave across a fleet. + +##### When to use it + +Use it when the same schedule can run in many places at once: reconnecting +clients, workers polling a shared queue, dashboard refreshes, health checks, +cache refills, or retries after a dependency outage. + +Decide the base shape first, then add jitter. For example, keep +`Schedule.exponential("100 millis")` as the retry shape or +`Schedule.spaced("5 seconds")` as the polling shape, then apply +`Schedule.jittered`. + +##### When not to use it + +Do not use jitter to hide unsafe retries. Non-idempotent writes, authorization +failures, validation failures, and malformed requests should be classified +before retrying. + +Avoid jitter when exact timing is the requirement, such as protocol heartbeats, +batch windows, deterministic tests, or user-facing countdowns. + +##### Schedule shape + +`Schedule.jittered` modifies the delay produced by another schedule. In Effect, +each delay is randomly adjusted between 80% and 120% of the original delay. It +does not decide what is retryable, how often to stop, or how many attempts are +allowed. Compose those decisions separately: + +- `Schedule.exponential` or `Schedule.spaced` describes the base cadence +- `Schedule.recurs` or `Schedule.during` bounds the recurrence +- `Schedule.jittered` spreads wake-ups around the base cadence + +##### Example + +```ts runnable +import { Console, Effect, Schedule } from "effect" + +type ApiError = { + readonly _tag: "ServiceUnavailable" + readonly client: string + readonly attempt: number +} + +const retryWithoutHerding = Schedule.exponential("20 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const runClient = (client: string) => { + let attempts = 0 + + const fetchSharedResource = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`${client} attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail({ + _tag: "ServiceUnavailable", + client, + attempt: attempts + }) + } + + return `${client} loaded the resource` + }) + + return fetchSharedResource.pipe( + Effect.retry(retryWithoutHerding), + Effect.flatMap(Console.log) + ) +} + +const program = Effect.forEach( + ["client-a", "client-b", "client-c"], + runClient, + { concurrency: 3, discard: true } +) + +Effect.runPromise(program) +// Output may vary: jitter and concurrent clients can change ordering +// client-a attempt 1 +// client-b attempt 1 +// client-c attempt 1 +// client-c attempt 2 +// client-b attempt 2 +// client-a attempt 2 +// client-b attempt 3 +// client-b loaded the resource +// client-c attempt 3 +// client-c loaded the resource +// client-a attempt 3 +// client-a loaded the resource +``` + +The first attempt for each client runs immediately. If a client fails, the +exponential schedule controls the retry shape and `Schedule.jittered` prevents +every client from sleeping for exactly the same delay. + +##### Variants + +For polling, jitter a `Schedule.spaced` repeat policy. For outage recovery, +combine exponential backoff, jitter, and an elapsed budget. For a hard +maximum-delay guarantee, apply jitter before the final `Schedule.modifyDelay` +cap. + +##### Notes and caveats + +Jitter reduces synchronization; it does not reduce the total number of attempts. +Keep attempt limits, elapsed-time budgets, rate limits, and error classification +visible near the effect being retried. + +Because jitter changes timing randomly, logs and metrics should be read as +ranges around the base policy rather than exact timestamps. + +#### 20.2 Coordinated clients + +Use jitter when clients share a start signal and would otherwise make follow-up +calls as one wave. + +##### Problem + +Coordinated clients start from the same signal and keep following the same +cadence. A deploy, cache expiry, feature flag flip, or upstream outage can make +hundreds of clients fail or poll together. If all of them retry after exactly +`100 millis`, then `200 millis`, then `400 millis`, a policy that is polite for +one client becomes noisy for the service receiving all of them. + +##### When to use it + +Use it for browser reconnects, service instances retrying the same dependency, +workers polling jobs created in batches, and scheduled processes released or +restarted together. The more actors share the cadence, the more useful jitter +becomes. + +##### When not to use it + +Do not add jitter when exact timing is the product or protocol requirement. A +metronomic heartbeat, fixed billing boundary, or protocol timeout may need a +predictable `Schedule.fixed` or `Schedule.spaced` cadence. + +Do not use jitter to disguise errors that should not be retried. Classify +validation failures, authorization failures, malformed requests, and unsafe +non-idempotent writes before applying the schedule. + +##### Schedule shape + +Choose the deterministic shape first, then jitter it: + +1. Start with the cadence: `Schedule.exponential`, `Schedule.spaced`, or another + base schedule. +2. Apply `Schedule.jittered` so each recurrence delay is spread around that + cadence. +3. Add limits such as `Schedule.recurs` or `Schedule.during`. + +That order keeps the policy readable: exponential retry, jittered, bounded. + +##### Example + +```ts runnable +import { Console, Effect, Schedule } from "effect" + +type ClientError = { + readonly _tag: "ClientError" + readonly client: string + readonly attempt: number +} + +const clientRetry = Schedule.exponential("25 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(5)) +) + +const makeClientCall = (client: string) => { + let attempts = 0 + + const call = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`${client} call ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail({ + _tag: "ClientError", + client, + attempt: attempts + }) + } + + return `${client} done` + }) + + return call.pipe( + Effect.retry(clientRetry), + Effect.flatMap(Console.log) + ) +} + +const program = Effect.forEach( + ["browser-a", "browser-b", "browser-c"], + makeClientCall, + { concurrency: 3, discard: true } +) + +Effect.runPromise(program) +// Output may vary: jitter and concurrent clients can change ordering +// browser-a call 1 +// browser-b call 1 +// browser-c call 1 +// browser-a call 2 +// browser-b call 2 +// browser-c call 2 +// browser-a call 3 +// browser-a done +// browser-b call 3 +// browser-b done +// browser-c call 3 +// browser-c done +``` + +Each client has the same retry policy, but each recurrence samples its own +jittered delay. The policy remains easy to review because the base cadence, +jitter, and retry limit are all visible. + +##### Variants + +Use `Schedule.exponential(...).pipe(Schedule.jittered)` for retries after +failures. Use `Schedule.spaced(...).pipe(Schedule.jittered)` for polling. Pair +jitter with `Schedule.recurs` for count limits or `Schedule.during` for +elapsed-time budgets. + +##### Notes and caveats + +`Effect.retry` feeds failures into the schedule. `Effect.repeat` feeds +successful values into the schedule. That distinction matters when adding +predicates: retry policies usually inspect errors, while polling policies +usually inspect returned statuses. + +Jitter reduces accidental coordination; it is not fairness or rate limiting. If +the downstream system needs a strict cap, use a rate limiter, queue, or server +side backpressure mechanism. + +#### 20.3 Recovery spikes + +Use jittered backoff after an outage so recovery traffic spreads out instead of +forming synchronized retry waves. + +##### Problem + +Recovery can become its own incident. If every process uses the same +deterministic retry sequence, retry waves can line up across the fleet just as a +dependency is trying to recover. + +The first attempt still happens normally. The schedule only decides what to do +after a typed failure is fed to `Effect.retry`. + +##### When to use it + +Use it when many clients, workers, pods, or service instances retry the same +dependency after broker restarts, database failovers, network partitions, +rollbacks, or regional control-plane incidents. + +This is a good default when the operation is safe to retry and the downstream +system benefits from recovery traffic being spread out. + +##### When not to use it + +Do not use jitter to make unsafe writes retryable. Classify validation errors, +authorization errors, malformed requests, and non-idempotent operations before +applying the schedule. + +Avoid jitter when timing itself is the contract, such as a fixed-rate heartbeat +that another system interprets precisely. + +##### Schedule shape + +Build the operational shape first, then jitter it: + +- `Schedule.exponential("200 millis")` creates the increasing recovery delay +- `Schedule.jittered` spreads each computed delay by 80% to 120% +- `Schedule.both(Schedule.recurs(6))` stops after a bounded number of retries + +##### Example + +```ts runnable +import { Console, Data, Effect, Schedule } from "effect" + +class DependencyUnavailable extends Data.TaggedError("DependencyUnavailable")<{ + readonly service: string + readonly instance: string + readonly attempt: number +}> {} + +const recoveryRetryPolicy = Schedule.exponential("30 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(6)) +) + +const recoverInstance = (instance: string) => { + let attempts = 0 + + const refreshFromDependency = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`${instance} refresh attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail( + new DependencyUnavailable({ + service: "orders-db", + instance, + attempt: attempts + }) + ) + } + + return `${instance} recovered` + }) + + return refreshFromDependency.pipe( + Effect.retry(recoveryRetryPolicy), + Effect.flatMap(Console.log) + ) +} + +const program = Effect.forEach( + ["instance-a", "instance-b", "instance-c"], + recoverInstance, + { concurrency: 3, discard: true } +) + +Effect.runPromise(program) +// Output may vary: jitter and concurrent clients can change ordering +// instance-a refresh attempt 1 +// instance-b refresh attempt 1 +// instance-c refresh attempt 1 +// instance-a refresh attempt 2 +// instance-c refresh attempt 2 +// instance-b refresh attempt 2 +// instance-c refresh attempt 3 +// instance-c recovered +// instance-a refresh attempt 3 +// instance-a recovered +// instance-b refresh attempt 3 +// instance-b recovered +``` + +Each instance keeps the same general backoff shape, but its individual delays +are randomly adjusted. The fleet no longer has to retry on identical boundaries +during recovery. + +##### Variants + +Use a smaller base delay when the dependency is local and cheap to probe. Use a +larger base delay when the dependency is expensive to warm up or has strict rate +limits. Add `Schedule.during` when the retry policy needs an elapsed recovery +budget. + +##### Notes and caveats + +Jitter is not a rate limiter. It spreads retry timing, but it does not enforce a +global concurrency limit or coordinate work across processes. Combine it with +downstream limits when the system needs hard protection. + +#### 20.4 Add jitter to exponential backoff + +Add jitter to exponential backoff when multiple callers can fail together and +retry the same dependency. + +##### Problem + +Exponential backoff reduces retry pressure over time, but callers that start +together can still retry together: 100 milliseconds later, 200 milliseconds +later, 400 milliseconds later, and so on. + +Place `Schedule.jittered` after the exponential schedule. It keeps the +exponential shape and randomizes each delay between 80% and 120% of the delay +chosen by `Schedule.exponential`. + +##### When to use it + +Use jittered exponential backoff for idempotent HTTP requests, queue operations, +cache lookups, database calls, and service-to-service requests where many +fibers, workers, or service instances may retry together. + +The larger the caller population, the more important it is to avoid identical +retry times. + +##### When not to use it + +Do not use jitter as a stopping condition. Add `Schedule.recurs`, `times`, an +elapsed-time limit, or another bound when the retry policy must be finite. + +Do not use jitter when exact timing is required. For deterministic tests, either +avoid jitter or assert bounds instead of exact delays. + +##### Schedule shape + +With a 10 millisecond base, exponential backoff produces 10 milliseconds, 20 +milliseconds, 40 milliseconds, and 80 milliseconds. After `Schedule.jittered`, +those delays become ranges: 8-12 milliseconds, 16-24 milliseconds, 32-48 +milliseconds, and 64-96 milliseconds. + +`Schedule.both(Schedule.recurs(4))` adds a finite retry count without changing +the jittered delay. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class GatewayUnavailable extends Data.TaggedError("GatewayUnavailable")<{ + readonly status: number +}> {} + +let attempts = 0 + +const callGateway: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`gateway attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail(new GatewayUnavailable({ status: 503 })) + } + + return "gateway response" +}) + +const jitteredBackoff = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const program = callGateway.pipe( + Effect.retry(jitteredBackoff), + Effect.tap((value) => Console.log(`success: ${value}`)) +) + +Effect.runPromise(program).then(() => undefined, console.error) +// Output: +// gateway attempt 1 +// gateway attempt 2 +// gateway attempt 3 +// gateway attempt 4 +// success: gateway response +``` + +The original call runs immediately. Each retry uses the exponential delay as a +base and then jitters that delay. If all four retries fail, the last typed +`GatewayUnavailable` is propagated. + +##### Variants + +Use a smaller base delay for latency-sensitive work. Use a gentler exponential +factor, such as `1.5`, when doubling grows too quickly. + +When only some typed failures should be retried, pass `Effect.retry({ schedule, +while })`. The predicate controls retry eligibility; the schedule controls +timing and count. + +##### Notes and caveats + +`Schedule.jittered` does not take a percentage argument. Effect uses the fixed +80% to 120% range. + +Place jitter after the delay shape you want to randomize. Additional composition +can then add limits, caps, predicates, or observability around the jittered +backoff. + +Jitter spreads retry attempts, but it does not cap exponential growth. Long +retry policies still need a cap and a retry limit that match the caller's +budget. + +#### 20.5 Avoid synchronized retries in clustered systems + +Clustered callers need retry policies that avoid sending every node back to the +same dependency at the same instant. + +##### Problem + +Nodes, pods, workers, or service clients can observe the same failure at roughly +the same time. A shared fixed delay or identical exponential policy can then +create retry waves on the same boundaries. + +Add `Schedule.jittered` to the retry schedule. Each caller keeps the same broad +backoff shape, but waits a slightly different amount before retrying. + +##### When to use it + +Use this when the same retry policy may run concurrently in many places: +service replicas, queue consumers, background workers, cluster members, or many +fibers calling the same downstream dependency. + +It fits temporary leader unavailability, rolling restarts, short network +partitions, overload responses, and connection pool exhaustion. + +##### When not to use it + +Do not use jitter as the only protection for a cluster that can generate more +retry traffic than the dependency can handle. Jitter reduces alignment, not the +number of callers. + +Do not add jitter to hide an unbounded or overly aggressive policy. Cluster +retry policies still need retry limits, timeouts, queue boundaries, circuit +breakers, rate limits, or other operational bounds where appropriate. + +##### Schedule shape + +`Schedule.exponential("15 millis")` produces 15 milliseconds, 30 milliseconds, +60 milliseconds, and so on. `Schedule.jittered` changes those to ranges around +each base delay. `Schedule.recurs(4)` stops each caller after four retries. + +The first execution is not delayed. The schedule is consulted only after a +typed failure. + +##### Example + +```ts runnable +import { Console, Data, Effect, Schedule } from "effect" + +class ClusterRequestError extends Data.TaggedError("ClusterRequestError")<{ + readonly nodeId: string + readonly reason: "Unavailable" | "Overloaded" | "Partitioned" | "InvalidRequest" +}> {} + +const isRetryableClusterError = (error: ClusterRequestError) => + error.reason === "Unavailable" || + error.reason === "Overloaded" || + error.reason === "Partitioned" + +const clusteredRetryPolicy = Schedule.exponential("15 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const heartbeatProgram = (nodeId: string) => { + let attempts = 0 + + const sendHeartbeat: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`${nodeId}: heartbeat attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail( + new ClusterRequestError({ + nodeId, + reason: "Overloaded" + }) + ) + } + + yield* Console.log(`${nodeId}: heartbeat accepted`) + }) + + return sendHeartbeat.pipe( + Effect.retry({ + schedule: clusteredRetryPolicy, + while: isRetryableClusterError + }) + ) +} + +const program = Effect.all([ + heartbeatProgram("node-a"), + heartbeatProgram("node-b"), + heartbeatProgram("node-c") +], { concurrency: "unbounded", discard: true }) + +Effect.runPromise(program).then(() => undefined, console.error) +// Output may vary: jitter and concurrent workers can change ordering +// node-a: heartbeat attempt 1 +// node-b: heartbeat attempt 1 +// node-c: heartbeat attempt 1 +// node-a: heartbeat attempt 2 +// node-b: heartbeat attempt 2 +// node-c: heartbeat attempt 2 +// node-a: heartbeat attempt 3 +// node-a: heartbeat accepted +// node-b: heartbeat attempt 3 +// node-b: heartbeat accepted +// node-c: heartbeat attempt 3 +// node-c: heartbeat accepted +``` + +Each node starts immediately and retries transient cluster errors with the same +base policy. Jitter spreads the retry delays, so the nodes are less likely to +retry in one coordinated burst. + +##### Variants + +For a clustered operation with a steady retry cadence, jitter `Schedule.spaced` +instead of `Schedule.exponential`. + +For a capped policy, compose the cap first and then add `Schedule.jittered` if +the capped delay should also be spread. A capped base delay of 5 seconds becomes +a jittered delay between 4 and 6 seconds. + +##### Notes and caveats + +`Schedule.jittered` uses Effect's fixed 80% to 120% range. + +Jitter changes retry timing, not retry eligibility. Keep using `while` or +`until` predicates when only some typed failures should be retried. + +`Schedule.recurs(4)` means four retries after the original attempt, not four +total executions. + +#### 20.6 More stability, less predictability + +Jitter keeps the base cadence visible while making each individual delay +approximate. The point is to protect aggregate load, not to make one caller more +precise. + +##### Problem + +Many clients, workers, or service instances can start the same schedule at the +same time after a deploy, outage, or restart. Without jitter, they can also wake +up together and send a burst of retries, cache refreshes, or polls to the same +dependency. + +The policy should weaken that alignment without hiding the intended cadence. +Operators can still describe the base delay and the jitter range; they should +not expect every caller to wake at the exact same offset. + +##### When to use it + +Use jitter when aggregate load matters more than exact per-caller timing: + +- retries from many clients after a transient outage +- cache warming from multiple application instances +- polling loops that would otherwise hit a service on the same boundary +- reconnect loops after a broker, database, or gateway interruption + +This fits idempotent operations, where repeating the same request does not +change correctness. The schedule still needs a count limit, time budget, or +external lifetime when the workflow must stop. + +##### When not to use it + +Do not use jitter when exact timing is part of the requirement. Fixed reporting +windows, protocol heartbeats with strict deadlines, tests that assert precise +delays, and workflows coordinated by a shared clock usually need deterministic +timing. + +Do not use jitter to make unsafe retries safe. Retried writes still need +idempotency, deduplication, transactions, or another domain-level guarantee. +Jitter changes when the next attempt happens; it does not change whether the +attempt is valid. + +##### Schedule shape + +Start with the cadence you would have used without jitter, then apply +`Schedule.jittered`. For example, adding `Schedule.jittered` after +`Schedule.spaced("1 second")` still says "one second between recurrences", but +each one-second delay is randomized to roughly 800 milliseconds through 1.2 +seconds. + +For an exponential policy, each computed exponential delay is jittered: + +| Base delay | Jittered delay range | +| ---------- | -------------------- | +| 200 ms | 160-240 ms | +| 400 ms | 320-480 ms | +| 800 ms | 640-960 ms | +| 1.6 s | 1.28-1.92 s | + +The benefit is smoother aggregate load. The tradeoff is less exact timing for +any one fiber or process. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Random, Ref, Schedule } from "effect" + +type GatewayError = { + readonly _tag: "GatewayError" + readonly attempt: number +} + +const refreshPolicy = Schedule.spaced("20 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(3)) +) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + + const refreshCacheEntry: Effect.Effect = Effect.gen( + function*() { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`refresh attempt ${attempt}`) + + if (attempt < 3) { + return yield* Effect.fail({ _tag: "GatewayError", attempt } as const) + } + + return "cache refreshed" + } + ) + + const result = yield* refreshCacheEntry.pipe( + Effect.retry(refreshPolicy), + Random.withSeed("cache-refresh-demo") + ) + + yield* Console.log(result) +}) + +Effect.runPromise(program) +// Output: +// refresh attempt 1 +// refresh attempt 2 +// refresh attempt 3 +// cache refreshed +``` + +`program` runs the cache refresh immediately, then retries around a 20 +millisecond cadence instead of exactly every 20 milliseconds. The seeded random +service makes the demo reproducible; production code usually uses the default +random service. + +##### Variants + +For transient service failures, combine exponential backoff with jitter. That +keeps the exponential shape while avoiding synchronized retry bursts around each +step. + +For periodic background work, jitter the steady cadence. This is useful when +many instances may poll approximately every 30 seconds, but the service should +not receive all polls on the same boundary. + +##### Notes and caveats + +`Schedule.jittered` does not accept a custom percentage. The implementation in +`packages/effect/src/Schedule.ts` adjusts each delay between 80% and 120% of the +original delay. + +Jitter changes only delays. It preserves the schedule output, input handling, +and stopping behavior. Add `Schedule.recurs`, `Schedule.take`, +`Schedule.during`, or another limit when the policy must be finite. + +With `Effect.retry`, the first attempt is not delayed. Jitter applies to waits +between retry attempts after typed failures. With `Effect.repeat`, jitter +applies to waits between successful repetitions. + +For tests, avoid asserting an exact jittered delay. Either keep the schedule +deterministic in that test or assert the allowed bounds. + +#### 20.7 When not to add jitter + +Jitter is useful for desynchronizing callers, but it is the wrong tool when exact +timing is part of the contract. + +##### Problem + +Before applying `Schedule.jittered`, decide what readers may rely on: an exact +cadence, or an approximate cadence around a base delay. A randomized recurrence +may run earlier or later than the wrapped schedule would, so it should be a +deliberate load-shaping choice. + +##### When to use it + +Skip jitter when the exact delay is meaningful: + +- a protocol heartbeat, maintenance tick, or sampling loop must run at a known + cadence +- a test needs deterministic virtual-time advancement +- a user-visible retry, refresh, or progress check should feel predictable +- a small single-instance loop has no fleet-wide synchronization problem + +In those cases, use the schedule that states the real timing requirement: +`Schedule.fixed` for wall-clock cadence, `Schedule.spaced` for a gap after work +finishes, `Schedule.exponential` for deterministic backoff, and +`Schedule.recurs`, `Schedule.take`, or `Schedule.during` for visible bounds. + +##### When not to use it + +Do not add `Schedule.jittered` just because a schedule repeats. A single worker +that drains a local queue every second does not need random timing unless it is +competing with other workers or protecting a shared dependency. A UI path that +promises "try again in 5 seconds" should not sometimes wait 4 seconds and +sometimes 6 seconds. A test that advances `TestClock` by exact intervals should +not depend on a randomized delay range. + +Also avoid jitter when the schedule is documenting an external contract. Cron +boundaries, billing windows, lease renewals, and protocol timeouts usually need +predictability more than desynchronization. + +##### Schedule shape + +Choose the deterministic shape first and leave it unjittered when precision is +the requirement. Use `Schedule.fixed` for wall-clock cadence, `Schedule.spaced` +for a gap after work finishes, `Schedule.exponential` for deterministic backoff, +and `Schedule.recurs`, `Schedule.take`, or `Schedule.during` for visible bounds. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Ref, Schedule } from "effect" + +const predictableStatusPolling = Schedule.spaced("50 millis").pipe( + Schedule.take(3) +) + +const program = Effect.gen(function*() { + const polls = yield* Ref.make(0) + + const pollUserVisibleStatus = Ref.updateAndGet(polls, (n) => n + 1).pipe( + Effect.tap((poll) => Console.log(`poll ${poll}: status is still visible`)), + Effect.as("visible") + ) + + const finalRecurrence = yield* pollUserVisibleStatus.pipe( + Effect.repeat(predictableStatusPolling) + ) + + yield* Console.log(`stopped after recurrence ${finalRecurrence}`) +}) + +Effect.runPromise(program) +// Output: +// poll 1: status is still visible +// poll 2: status is still visible +// poll 3: status is still visible +// poll 4: status is still visible +// stopped after recurrence 3 +``` + +The loop uses a deterministic gap and a deterministic stop condition. Adding +`Schedule.jittered` would change the user-visible rhythm without improving +safety for this single-user workflow. + +##### Variants + +For tests, prefer deterministic schedules and advance virtual time by the exact +delay the schedule promises. Test jittered policies separately by asserting +that delays stay within Effect's `80%` to `120%` jitter range instead of +asserting one exact delay. + +For exact wall-clock cadence, prefer `Schedule.fixed`. For "wait this long +after the previous run finishes", prefer `Schedule.spaced`. For a small +single-instance loop, start with the simplest deterministic cadence and add +jitter only after there is an actual coordination or downstream-load problem. + +##### Notes and caveats + +`Schedule.jittered` changes only the recurrence delay. It does not change which +errors are retryable, when a schedule stops, or whether a repeated operation is +safe. If the problem is overload, quota enforcement, or too many concurrent +callers, jitter may be one useful tool, but it is not a replacement for limits, +classification, or admission control. + +### 21. Jitter in Real Systems + +#### 21.1 Jittered retries for HTTP clients + +Use jittered retries when many HTTP clients may see the same transient failure +and retry at nearly the same time. `Schedule.jittered` keeps the chosen retry +shape visible while spreading each retry delay across a small random range. + +##### Problem + +An HTTP call can fail because a gateway is overloaded, a request times out, or a +server returns `408`, `429`, or `5xx`. Retrying can help, but only when the +request is safe to repeat. For writes, "safe" usually means idempotent: running +the same request more than once has the same external effect as running it once. + +##### When to use it + +Use it for service-to-service calls, background delivery, and webhooks where a +shared outage can affect many callers. Keep error classification close to the +HTTP operation so the retry policy only sees failures that are worth retrying. + +##### When not to use it + +Do not retry validation errors, malformed requests, authentication failures, or +ordinary `4xx` responses. Do not blindly retry a `POST` that charges a card, +sends an email, or creates external state unless it carries an idempotency key or +another deduplication guarantee. + +Jitter also does not replace explicit rate-limit handling. If the server returns +a `Retry-After` value, prefer that server-provided delay for that response. + +##### Schedule shape + +Choose the backoff first, then add jitter. `Schedule.exponential("100 millis")` +produces increasing delays. `Schedule.jittered` modifies each selected delay to +a random value between 80% and 120% of the original delay. Add +`Schedule.recurs` or a time budget so the retry is bounded. + +##### Example + +```ts runnable deterministic +import { Data, Effect, Schedule } from "effect" + +type HttpMethod = "GET" | "HEAD" | "PUT" | "DELETE" | "POST" + +class HttpError extends Data.TaggedError("HttpError")<{ + readonly method: HttpMethod + readonly status: number + readonly idempotencyKey?: string +}> {} + +const isRetryableStatus = (status: number) => status === 408 || status === 429 || status >= 500 + +const isRetrySafe = (error: HttpError) => + isRetryableStatus(error.status) && + ( + error.method === "GET" || + error.method === "HEAD" || + error.method === "PUT" || + error.method === "DELETE" || + error.idempotencyKey !== undefined + ) + +let attempt = 0 + +const getProfile = Effect.gen(function*() { + attempt += 1 + yield* Effect.sync(() => console.log(`GET /profile attempt ${attempt}`)) + + if (attempt < 3) { + return yield* Effect.fail( + new HttpError({ method: "GET", status: 503 }) + ) + } + + return { id: 123, name: "Ada" } +}) + +const httpRetryPolicy = Schedule.exponential("20 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(5)) +) + +const program = getProfile.pipe( + Effect.retry({ + schedule: httpRetryPolicy, + while: isRetrySafe + }), + Effect.tap((profile) => Effect.sync(() => console.log(`loaded ${profile.name}`))) +) + +Effect.runPromise(program) +// Output: +// GET /profile attempt 1 +// GET /profile attempt 2 +// GET /profile attempt 3 +// loaded Ada +``` + +##### Variants + +For user-facing calls, use fewer retries or a short elapsed-time budget so the +caller gets a timely answer. For background delivery, use a larger base delay +and retry limit, but keep a clear handoff to a dead-letter queue, alert, or +operator-visible failed state. + +For writes, keep the safety check stricter than the timing check. Retrying a +`POST` can be reasonable when the downstream service honors an idempotency key. +Without that guarantee, surface the failure instead of risking duplicate side +effects. + +##### Notes and caveats + +`Effect.retry` runs the original request immediately. Jitter affects only waits +between retries after typed failures. + +`Schedule.jittered` changes delay only. Keep retry classification, maximum retry +count, and any total time budget explicit. + +#### 21.2 Jittered retries for Redis reconnects + +Use jittered retries for Redis reconnect loops that may run across many workers +at once. Pick the reconnect backoff first, then jitter the delay so workers do +not all reconnect on the same boundary. + +##### Problem + +A worker loses its Redis connection during a restart, failover, or short network +drop. It should reconnect quickly at first, back off after repeated failures, +and stop after a bounded number of attempts so the supervisor can report a real +outage. + +##### When to use it + +Use it for workers, stream consumers, subscription listeners, cache warmers, and +queue processors where reconnecting is expected. It is most useful when many +instances share the same Redis cluster. + +##### When not to use it + +Do not retry configuration errors. A bad Redis URL, missing credentials, TLS +misconfiguration, or an unsupported protocol setting should fail fast and be +reported as an operational problem. + +Do not use reconnect backoff as a substitute for connection limits, +health-checking, or graceful shutdown. The schedule controls timing only; it +does not decide whether the process should keep accepting work while Redis is +unavailable. + +##### Schedule shape + +`Schedule.exponential("100 millis")` gives the reconnect loop a short first +delay and doubles the delay after each failed reconnect. `Schedule.jittered` +then randomizes each computed delay between `80%` and `120%` of that delay. + +Apply a cap after jitter if the final sleep must never exceed a configured +maximum. Keep the retry count separate from the delay shape: +`Schedule.recurs(8)` means at most eight retries after the original reconnect +attempt. + +##### Example + +```ts runnable deterministic +import { Data, Duration, Effect, Schedule } from "effect" + +class RedisReconnectError extends Data.TaggedError("RedisReconnectError")<{ + readonly reason: "timeout" | "connection-refused" | "server-loading" +}> {} + +let attempt = 0 + +const reconnectRedis = Effect.gen(function*() { + attempt += 1 + yield* Effect.sync(() => console.log(`redis reconnect attempt ${attempt}`)) + + if (attempt < 4) { + return yield* Effect.fail( + new RedisReconnectError({ reason: "server-loading" }) + ) + } + + yield* Effect.sync(() => console.log("redis reconnected")) +}) + +const redisReconnectPolicy = Schedule.exponential("20 millis").pipe( + Schedule.jittered, + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.millis(120)))), + Schedule.both(Schedule.recurs(8)) +) + +const program = reconnectRedis.pipe( + Effect.retry(redisReconnectPolicy) +) + +Effect.runPromise(program) +// Output: +// redis reconnect attempt 1 +// redis reconnect attempt 2 +// redis reconnect attempt 3 +// redis reconnect attempt 4 +// redis reconnected +``` + +##### Variants + +For a startup path, keep the first delay small but use a short retry limit so the +service can fail readiness quickly when Redis is not reachable. + +For a long-running background worker, use a larger retry limit or combine the +policy with `Schedule.during` to express an elapsed reconnect budget. That gives +operators a concrete answer to how long the worker will keep trying before it +surfaces the failure. + +For a large fleet, keep jitter enabled even when the cap is low. The cap limits +maximum wait time; jitter reduces synchronization. + +##### Notes and caveats + +`Effect.retry` feeds the `RedisReconnectError` into the schedule after a failed +reconnect attempt. The schedule decides whether to try again and how long to +sleep before that next attempt. + +`Schedule.exponential` recurs forever by itself. Always pair it with a limit +such as `Schedule.recurs`, `Schedule.take`, `Schedule.during`, or a predicate +that stops on non-retryable Redis errors. + +Apply `Schedule.jittered` to the chosen cadence rather than hiding randomness in +the reconnect effect. Keeping jitter in the schedule makes the retry contract +reviewable: exponential backoff for pressure, a cap for maximum sleep, and +jitter for fleet-wide spreading. + +#### 21.3 Jittered retries for WebSocket reconnect + +Use a bounded, jittered backoff when many WebSocket clients may reconnect after +the same gateway restart, network flap, or load-balancer rotation. + +##### Problem + +A reconnect loop should recover from transient close or connect failures without +leaving a user-facing client in an indefinite "reconnecting" state. The policy +must show which failures are retryable, how the delay grows, and where the loop +stops. + +##### When to use it + +Use this recipe when reconnecting an idempotent WebSocket session after +transient network or server conditions: temporary gateway unavailability, +connection reset, abnormal close, server overload, or a rolling restart. + +It is especially useful when many clients run the same reconnect code: browser +tabs, mobile apps, desktop clients, edge workers, or service replicas that keep +long-lived sockets open. + +##### When not to use it + +Do not retry authentication, authorization, protocol, or validation failures as +if they were transient. An expired token should usually refresh credentials +first. A forbidden user, unsupported protocol version, malformed URL, or rejected +subprotocol should fail in the domain layer before the reconnect schedule is +used. + +Do not treat jitter as admission control. Jitter spreads reconnect attempts, but +it does not reduce the number of clients that will try. Large fleets still need +server-side limits, connection draining, backpressure, and user-visible fallback +states. + +##### Schedule shape + +Start with exponential backoff, apply `Schedule.jittered`, cap the final delay +with `Schedule.modifyDelay`, and add a retry limit with `Schedule.recurs`. +`Schedule.jittered` adjusts each computed delay between 80% and 120% of the +wrapped schedule's delay. A 1 second reconnect delay therefore becomes 800 +milliseconds to 1.2 seconds. + +##### Example + +```ts runnable deterministic +import { Data, Duration, Effect, Schedule } from "effect" + +class WebSocketConnectError extends Data.TaggedError("WebSocketConnectError")<{ + readonly reason: + | "AbnormalClose" + | "GatewayUnavailable" + | "NetworkError" + | "ServerOverloaded" + | "Unauthorized" + | "UnsupportedProtocol" +}> {} + +const isRetryableReconnectError = (error: WebSocketConnectError): boolean => { + switch (error.reason) { + case "AbnormalClose": + case "GatewayUnavailable": + case "NetworkError": + case "ServerOverloaded": + return true + case "Unauthorized": + case "UnsupportedProtocol": + return false + } +} + +let attempt = 0 + +const connectWebSocket = Effect.gen(function*() { + attempt += 1 + yield* Effect.sync(() => console.log(`websocket connect attempt ${attempt}`)) + + if (attempt === 1) { + return yield* Effect.fail( + new WebSocketConnectError({ reason: "GatewayUnavailable" }) + ) + } + if (attempt === 2) { + return yield* Effect.fail( + new WebSocketConnectError({ reason: "NetworkError" }) + ) + } + + yield* Effect.sync(() => console.log("websocket connected")) +}) + +const reconnectPolicy = Schedule.exponential("20 millis").pipe( + Schedule.jittered, + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.millis(100)))), + Schedule.both(Schedule.recurs(8)) +) + +const program = connectWebSocket.pipe( + Effect.retry({ + schedule: reconnectPolicy, + while: isRetryableReconnectError + }) +) + +Effect.runPromise(program) +// Output: +// websocket connect attempt 1 +// websocket connect attempt 2 +// websocket connect attempt 3 +// websocket connected +``` + +The sample uses short delays so it terminates quickly when pasted into +`scratchpad/repro.ts`. The same shape can use larger production intervals. + +##### Variants + +For an interactive screen, keep the retry count and cap small enough that the UI +can move to a visible "reconnect failed" state quickly. + +For a background client, allow a longer tail but keep the cap explicit and emit +attempt telemetry around the reconnect effect. Operators usually need to know +the close reason, attempt count, and final exhausted failure. + +If the server sends a reconnect hint, such as a close reason with a retry-after +duration, prefer that server-provided delay for that case. Use the jittered +exponential policy as the fallback when the client has no better timing signal. + +##### Notes and caveats + +`Schedule.jittered` has fixed bounds in Effect. It adjusts delays between 80% +and 120% of the original delay; this recipe does not assume configurable jitter +bounds. + +`Effect.retry` feeds typed failures into the schedule. The first connect attempt +is not delayed. Jitter affects only reconnect delays after failures. + +`Schedule.recurs(8)` means eight retries after the original connect attempt, not +eight total executions. + +Reconnect safety is still a domain concern. Refresh credentials before retrying +authorization failures, avoid replaying non-idempotent session setup without a +deduplication story, and keep user-facing timeout or fallback behavior close to +the caller. + +#### 21.4 Jittered periodic refresh + +Use jittered repetition when a refresh loop should run on a recognizable cadence +without making every instance hit the same dependency at the same moment. + +##### Problem + +Each service instance refreshes cached configuration in the background. The +first refresh should run immediately. Later refreshes should stay near the +chosen interval while drifting enough to avoid synchronized requests across the +fleet. + +##### When to use it + +Use this when many independent processes, fibers, clients, or browser sessions +repeat the same successful operation on a regular cadence. + +It fits refresh loops for cached configuration, feature flags, service +discovery data, quota snapshots, and other state that should stay reasonably +fresh but does not need to update on an exact wall-clock boundary. + +Use it when the base interval is still the operational contract. A one-minute +refresh remains a one-minute refresh in spirit, but individual recurrences are +spread around that value. + +##### When not to use it + +Do not use jitter when the refresh must happen at exact wall-clock boundaries, +such as a report that must run at the top of every hour. + +Do not use jitter as the only protection for an overloaded dependency. Jitter +reduces synchronization; it does not enforce quotas, backpressure, admission +control, or a maximum number of concurrent refreshes. + +Do not use this schedule to recover from refresh failures. With +`Effect.repeat`, the schedule sees successful refresh results. If loading +configuration can fail transiently, give the refresh effect its own retry +policy before repeating it. + +##### Schedule shape + +Start with the intended refresh cadence and apply jitter to that cadence: + +`Schedule.spaced("1 minute")` waits one minute after each successful refresh +before starting the next one. `Schedule.jittered` randomly adjusts each +recurrence delay between 80% and 120% of the original delay, so a one-minute +interval becomes a delay between 48 and 72 seconds. + +The first refresh is not delayed by the schedule. It runs when the effect +starts. The schedule controls only the recurrences after successful refreshes. + +##### Example + +```ts runnable deterministic +import { Effect, Schedule } from "effect" + +type Config = { + readonly version: string + readonly cacheTtlMillis: number + readonly featureFlags: ReadonlyArray +} + +let version = 0 + +const loadConfig = Effect.sync((): Config => { + version += 1 + console.log(`loaded config version ${version}`) + return { + version: `v${version}`, + cacheTtlMillis: 60_000, + featureFlags: ["search", "checkout"] + } +}) + +const replaceCachedConfig = (config: Config) => + Effect.sync(() => { + console.log(`cached ${config.version}`) + }) + +const refreshCachedConfig = loadConfig.pipe( + Effect.flatMap(replaceCachedConfig) +) + +const demoRefreshSchedule = Schedule.spaced("20 millis").pipe( + Schedule.jittered, + Schedule.take(3) +) + +const program = refreshCachedConfig.pipe( + Effect.repeat(demoRefreshSchedule), + Effect.tap(() => Effect.sync(() => console.log("refresh loop stopped"))) +) + +Effect.runPromise(program) +// Output: +// loaded config version 1 +// cached v1 +// loaded config version 2 +// cached v2 +// loaded config version 3 +// cached v3 +// loaded config version 4 +// cached v4 +// refresh loop stopped +``` + +The sample uses a short interval and `Schedule.take(3)` so it terminates +quickly. For a real background fiber, use the operational interval, such as one +minute, and tie interruption to the process lifecycle. + +##### Variants + +Use a longer interval when stale configuration is acceptable and the shared +configuration service is expensive. Use `Schedule.tapOutput` when you want +telemetry for the repeat count. + +If a refresh can fail transiently, retry the single refresh operation with its +own short policy, then repeat the recovered operation on the longer refresh +cadence. That keeps failure recovery separate from normal periodic repetition. + +##### Notes and caveats + +`Schedule.jittered` does not expose configurable bounds. In Effect, it adjusts +each recurrence delay between 80% and 120% of the original delay. + +`Effect.retry` feeds failures into a schedule. `Effect.repeat` feeds successful +values into a schedule. For periodic refresh, jitter usually belongs on the +repeat schedule because the goal is to spread normal successful polling or +refresh traffic. + +`Schedule.spaced` measures the delay after the previous refresh completes. If +the refresh itself takes several seconds, the next refresh starts after the +work completes and the jittered delay has elapsed. + +#### 21.5 Jittered cache warming + +Cache warming is successful background work that repeats on a cadence. Use +jitter so many instances do not refresh the same hot keys at the same moment. + +##### Problem + +Every instance should run its first warming pass when the process starts and +then refresh important cache entries roughly every thirty seconds. In a fleet, +later passes should drift enough that instances do not all read the same backing +services at once. + +##### When to use it + +Use this when many service instances, workers, or pods run the same cache +warming loop against the same backing store, database, object store, or +downstream API. + +It fits background warming for product catalogs, feature snapshots, permission +lookups, pricing tables, routing data, and other data that should remain hot +but does not need to refresh on an exact wall-clock boundary. + +Use it when "roughly every thirty seconds" is acceptable and a steadier load +profile matters more than every instance refreshing at the same moment. + +##### When not to use it + +Do not use jitter when cache entries must be refreshed at exact wall-clock +boundaries, such as a report cache rebuilt at the top of every hour. + +Do not use jitter as the only overload control for expensive warming work. +Limit concurrency inside the warming effect, use downstream rate limits, and +bound the number of keys each pass warms. + +Do not use the repeat schedule to classify warming failures. With +`Effect.repeat`, the schedule sees successful warming results. If a single +warming pass can fail transiently, retry that pass separately before repeating +it on the long-running cadence. + +##### Schedule shape + +Start with the intended warming interval and add jitter: + +`Schedule.spaced("30 seconds")` waits thirty seconds after a successful warming +pass completes. `Schedule.jittered` randomly adjusts each recurrence delay +between 80% and 120% of the original delay, so a thirty-second interval becomes +a delay between 24 and 36 seconds. + +The first warming pass is not delayed by the schedule. It runs when the effect +starts. The schedule controls only the recurrences after successful warming +passes. + +##### Example + +```ts runnable deterministic +import { Effect, Schedule } from "effect" + +type CacheKey = string + +const hotKeys: ReadonlyArray = [ + "catalog:featured", + "pricing:default", + "permissions:public" +] + +const warmCacheEntry = Effect.fnUntraced(function*(key: CacheKey) { + yield* Effect.sync(() => console.log(`warmed ${key}`)) +}) + +const warmCacheOnce = Effect.forEach( + hotKeys, + warmCacheEntry, + { concurrency: 4 } +).pipe( + Effect.asVoid +) + +const demoWarmingSchedule = Schedule.spaced("20 millis").pipe( + Schedule.jittered, + Schedule.take(2) +) + +const program = warmCacheOnce.pipe( + Effect.repeat(demoWarmingSchedule), + Effect.tap(() => Effect.sync(() => console.log("cache warming stopped"))) +) + +Effect.runPromise(program) +// Output: +// warmed catalog:featured +// warmed pricing:default +// warmed permissions:public +// warmed catalog:featured +// warmed pricing:default +// warmed permissions:public +// warmed catalog:featured +// warmed pricing:default +// warmed permissions:public +// cache warming stopped +``` + +The sample warms three keys immediately and then performs two scheduled +recurrences. Use the real interval and lifecycle interruption for a production +background warmer. + +##### Variants + +Retry transient failures inside one warming pass, then repeat the recovered +warming pass on the longer jittered cadence. This keeps two policies separate: +a short retry policy for a failed warming attempt, and a long repeat policy for +normal cache warming. + +Use a longer base interval for expensive data or large fleets. The jitter range +follows the base delay; a five-minute interval becomes a delay between four and +six minutes. + +##### Notes and caveats + +`Schedule.jittered` does not expose configurable bounds. In Effect, it adjusts +each recurrence delay between 80% and 120% of the original delay. + +`Effect.retry` feeds failures into a schedule. `Effect.repeat` feeds successful +values into a schedule. Cache warming usually uses jitter on the repeat +schedule because the goal is to spread normal successful background traffic. + +`Schedule.spaced` measures the delay after the previous warming pass completes. +If warming a large key set takes ten seconds, the next pass starts after that +work completes and the jittered delay has elapsed. + +## Part VI — Composition and Termination + +### 22. Stop Conditions + +#### 22.1 Stop when status becomes terminal + +Poll a job, order, import, deployment, or other long-running workflow when the +status endpoint succeeds even while the workflow is still in progress. A later +successful status eventually means "there is nothing more to poll". + +Use `Effect.repeat` for the polling loop and let the schedule inspect each +successful status. The effect performs the first status read immediately. After +that, the schedule decides whether to wait and read again. + +##### Problem + +A status API may return `"queued"` or `"running"` as successful responses before +it eventually returns `"completed"`, `"failed"`, or `"canceled"`. The repeat +policy should treat only the non-terminal statuses as reasons to poll again. + +A domain status such as `"failed"` is still a successful response from the status +API. The schedule should observe that value and stop the repeat loop, while +transport or decoding failures remain ordinary Effect failures. + +##### When to use it + +Use this when the repeated effect returns a domain status value and only some +of those statuses mean "poll again". + +This is a good fit for order fulfillment, export generation, provisioning, +replication, and back-office jobs where states such as `"queued"` or +`"running"` are normal intermediate observations, while states such as +`"completed"`, `"failed"`, or `"canceled"` are terminal observations. + +##### When not to use it + +Do not use this as a retry policy for failed status reads. With +`Effect.repeat`, a failure from the status-read effect stops the repeat before +the schedule can inspect a status. Add a separate retry around the status read +if transient read failures should be retried. + +Do not encode normal terminal statuses as failures just to stop polling. If the +remote workflow can end in `"completed"` or `"failed"` and both are meaningful +business outcomes, return both as successful status values and interpret the +final status after the repeat completes. + +Do not leave production polling unbounded unless the fiber has a clear owner +that can interrupt it. Add a recurrence limit or elapsed budget when a terminal +status is expected but not guaranteed. + +##### Schedule shape + +Combine `Schedule.identity()` with a cadence using +`Schedule.bothLeft`. The identity schedule makes the latest successful status +the schedule output, while the cadence supplies the delay before the next read. +Then use `Schedule.while` to continue only while that status is not terminal. + +Returning `true` from the predicate allows another poll. Returning `false` +stops the repeat and returns the latest status. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type OrderStatus = + | { readonly state: "queued"; readonly orderId: string } + | { readonly state: "running"; readonly orderId: string; readonly step: string } + | { readonly state: "completed"; readonly orderId: string; readonly receiptId: string } + | { readonly state: "failed"; readonly orderId: string; readonly reason: string } + | { readonly state: "canceled"; readonly orderId: string } + +type StatusReadError = { + readonly _tag: "StatusReadError" + readonly orderId: string +} + +const statuses: ReadonlyArray = [ + { state: "queued", orderId: "order-123" }, + { state: "running", orderId: "order-123", step: "packing" }, + { state: "completed", orderId: "order-123", receiptId: "receipt-456" } +] + +let reads = 0 + +const readOrderStatus = ( + orderId: string +): Effect.Effect => + Effect.gen(function*() { + const index = yield* Effect.sync(() => { + const current = reads + reads += 1 + return current + }) + const status = statuses[index] ?? statuses[statuses.length - 1]! + + yield* Console.log(`status read ${index + 1}: ${status.state}`) + return status + }) + +const isTerminal = (status: OrderStatus): boolean => + status.state === "completed" || + status.state === "failed" || + status.state === "canceled" + +const pollUntilTerminal = Schedule.identity().pipe( + Schedule.bothLeft(Schedule.spaced("100 millis")), + Schedule.while(({ output }) => !isTerminal(output)) +) + +const waitForTerminalOrderStatus = (orderId: string) => + readOrderStatus(orderId).pipe( + Effect.repeat(pollUntilTerminal) + ) + +const program = waitForTerminalOrderStatus("order-123").pipe( + Effect.flatMap((status) => Console.log(`final status: ${status.state}`)) +) + +Effect.runPromise(program) +// Output: +// status read 1: queued +// status read 2: running +// status read 3: completed +// final status: completed +``` + +`waitForTerminalOrderStatus` reads the status immediately. If the first status +is `"completed"`, `"failed"`, or `"canceled"`, there is no delay and no second +request. If the status is `"queued"` or `"running"`, the schedule waits two +seconds in a real policy before the next read; the runnable example uses a +shorter delay so it finishes quickly. + +The returned effect succeeds with the last observed `OrderStatus`. Usually that +will be a terminal status. If you add a recurrence cap or elapsed budget, the +final value may still be `"queued"` or `"running"`, so check the final status +before treating the workflow as complete. + +##### Variants + +For an internal worker where eventual completion is expected, combine the +condition with `Schedule.recurs`. For caller-facing polling, combine it with +`Schedule.during` so the caller gets a bounded answer. + +For many clients polling the same kind of resource, add `Schedule.jittered` +after choosing a base cadence so instances do not synchronize. + +The polling schedule answers "should I observe again?" The code after polling +answers "what does the final status mean for this caller?" + +##### Notes and caveats + +The first status read is not delayed. Schedule delays apply only before later +recurrences. + +`Schedule.while` is evaluated at recurrence decision points after successful +runs. It does not interrupt a status read that is already running. + +`Effect.repeat` feeds successful values into the schedule. `Effect.retry` feeds +failures into the schedule. Use `Effect.repeat` when the status value itself +decides whether to continue polling. + +When a timing schedule needs to inspect the repeated effect's successful value, +use a schedule whose input type matches that value. `Schedule.identity()` is +convenient when the caller should receive the last observed value rather than the +timing schedule's own output. + +#### 22.2 Stop when no more work remains + +Use this pattern when each successful run reports whether more work is waiting. +The schedule should repeat while that report says work remains, then return the +last successful observation when the queue is empty. + +This is a common shape for queue drains, batch processors, catch-up workers, and +maintenance jobs that should keep going while they are making progress, but +should stop cleanly when there is nothing left to do. + +##### Problem + +A queue-drain effect processes one bounded batch and returns a result such as +`{ processed, remaining }`. One run is always useful because it discovers the +current backlog; after that, the schedule should continue only while `remaining` +says work is left. + +Keep that decision in the schedule so reviewers can see both the cadence and the +termination rule, instead of finding a mutable loop counter or sleep hidden +inside the worker. + +##### When to use it + +Use this recipe when the successful value contains a clear "remaining work" +signal, such as a queue depth, a continuation cursor, or a `hasMore` flag. + +It is a good fit when each recurrence should wait before taking another batch. +That prevents a catch-up worker from turning a large backlog into a tight loop +that competes with foreground traffic. + +##### When not to use it + +Do not use this as a replacement for queue acknowledgement, leasing, or +visibility timeout rules. The effect that drains the queue still owns those +delivery semantics. + +Do not use this schedule to classify worker failures. `Effect.repeat` feeds +successful values into the schedule. Failures from the drain effect fail the +whole repeat unless you handle or retry them separately. + +Avoid this shape when the queue can notify workers directly. A push signal, +stream, or consumer loop may be a better model than scheduled draining. + +##### Schedule shape + +Use `Schedule.identity()` to keep the latest successful drain +result as the schedule output, combine it with a spacing policy, and continue +only while `remaining` is greater than zero. + +`Schedule.while` receives metadata for each successful step. Returning `true` +continues the schedule; returning `false` stops it and yields the latest output. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type DrainResult = { + readonly processed: number + readonly remaining: number +} + +type QueueDrainError = { + readonly _tag: "QueueDrainError" + readonly message: string +} + +const batches: ReadonlyArray = [ + { processed: 25, remaining: 40 }, + { processed: 25, remaining: 15 }, + { processed: 15, remaining: 0 } +] + +let drains = 0 + +const drainWorkQueue: Effect.Effect = Effect.gen(function*() { + const index = yield* Effect.sync(() => { + const current = drains + drains += 1 + return current + }) + const result = batches[index] ?? batches[batches.length - 1]! + + yield* Console.log( + `drain ${index + 1}: processed=${result.processed}, remaining=${result.remaining}` + ) + return result +}) + +const drainUntilEmpty = Schedule.identity().pipe( + Schedule.bothLeft(Schedule.spaced("100 millis")), + Schedule.while(({ output }) => output.remaining > 0) +) + +const program = drainWorkQueue.pipe( + Effect.repeat(drainUntilEmpty), + Effect.flatMap((result) => Console.log(`stopped with ${result.remaining} items remaining`)) +) + +Effect.runPromise(program) +// Output: +// drain 1: processed=25, remaining=40 +// drain 2: processed=25, remaining=15 +// drain 3: processed=15, remaining=0 +// stopped with 0 items remaining +``` + +`drainWorkQueue` runs once immediately. If that first drain returns +`remaining: 0`, the schedule stops without waiting and `runDrain` succeeds with +that result. + +If the first drain returns `remaining: 120`, the schedule waits before running +another drain. The example uses a short delay so it finishes quickly; production +drainers often use a longer cadence. The final result is the first observation +whose `remaining` value is `0`. + +##### Variants + +Use `remaining > 0 && processed > 0` when the worker should stop if a run made +no progress, even if the queue still reports backlog. That avoids repeating +forever when work is stuck behind a poison item or unavailable partition. + +Use a longer interval for background maintenance queues, or a shorter interval +for interactive catch-up work. The spacing is paid after each successful drain, +so long-running batches naturally push the next recurrence later. + +Add a separate limit when an empty queue is not guaranteed. For example, combine +the drain condition with `Schedule.recurs` or `Schedule.during` when the worker +has a fixed maintenance window. + +##### Notes and caveats + +`Effect.repeat` always performs the original effect before the schedule controls +any recurrence. The schedule decides whether to run again after observing the +successful `DrainResult`. + +`Schedule.while` inspects successful values only. If `drainWorkQueue` fails, the +repeat fails unless the effect handles the error or the whole drain is wrapped +in a retry policy. + +`Schedule.identity()` is what makes `runDrain` return the latest +`DrainResult`. Without preserving the domain value, the result would come from a +timing schedule, such as the numeric output of `Schedule.spaced`. + +Keep the reported `remaining` value meaningful. If it is approximate, stale, or +eventually consistent, add an operational guard such as an elapsed budget or a +recurrence cap so the worker cannot repeat forever on bad telemetry. + +#### 22.3 Stop when data becomes available + +Sometimes the absence of data is not an error. A cache entry may be warming in +the background, a resource record may be propagating, or another process may be +about to publish the value you need. In those cases, model "not available yet" +as a successful observation and let the schedule stop when a later successful +observation contains the data. + +This recipe uses a cache lookup as the example. The lookup can succeed with +`Missing` or `Available`; only real lookup failures stay in the error channel. + +##### Problem + +A profile lookup may hit a cache before the background warmer has published the +entry. A miss is a normal observation in that path, so the polling policy should +wait briefly for an `Available` result without converting `Missing` into an +error. + +The first lookup should happen immediately. If the data is missing, wait and try +again. If the data is available, stop without another lookup. The schedule should +make that stop condition visible in one place. + +##### When to use it + +Use this when all of these are true: + +- A missing value is a normal temporary result. +- Some other path is already responsible for making the data available. +- The caller wants to wait by polling rather than subscribing to a push signal. +- Lookup failures should remain distinct from "not available yet". + +Typical examples include asynchronous cache warm-up, read-through cache +population, eventually visible resource metadata, and short propagation windows +after a write. + +##### When not to use it + +Do not use this when the data may never be produced. Invalid keys, authorization +problems, disabled producers, and malformed requests should be represented as +separate domain results or failures before the schedule is applied. + +Do not treat cache backend errors as misses unless the domain explicitly says +that is safe. A network error, serialization failure, or unavailable cache +server is usually a failed lookup, not an absent value. + +Prefer a push-based callback, queue message, or notification channel when the +producer already has a reliable way to signal availability. + +##### Schedule shape + +Use a spaced schedule for the polling cadence, preserve the successful lookup +result as the schedule output, and continue only while that result is +`Missing`. The repeated program can then inspect the final observed lookup +result after the schedule stops. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type Availability = + | { readonly _tag: "Missing" } + | { readonly _tag: "Available"; readonly value: A } + +interface UserProfile { + readonly id: string + readonly displayName: string +} + +type CacheLookupError = { + readonly _tag: "CacheLookupError" + readonly message: string +} + +const observations: ReadonlyArray> = [ + { _tag: "Missing" }, + { _tag: "Missing" }, + { + _tag: "Available", + value: { id: "user-1", displayName: "Ada" } + } +] + +let lookups = 0 + +const lookupProfileCache = ( + userId: string +): Effect.Effect, CacheLookupError> => + Effect.gen(function*() { + const index = yield* Effect.sync(() => { + const current = lookups + lookups += 1 + return current + }) + const observation = observations[index] ?? observations[observations.length - 1]! + + yield* Console.log(`${userId} cache lookup ${index + 1}: ${observation._tag}`) + return observation + }) + +const pollUntilAvailable = Schedule.identity>().pipe( + Schedule.bothLeft(Schedule.spaced("100 millis")), + Schedule.while(({ output }) => output._tag === "Missing") +) + +const waitForProfile = ( + userId: string +): Effect.Effect< + UserProfile, + CacheLookupError | { readonly _tag: "ProfileUnavailable" } +> => + lookupProfileCache(userId).pipe( + Effect.repeat(pollUntilAvailable), + Effect.flatMap((availability) => + availability._tag === "Available" + ? Effect.succeed(availability.value) + : Effect.fail({ _tag: "ProfileUnavailable" as const }) + ) + ) + +const program = waitForProfile("user-1").pipe( + Effect.flatMap((profile) => Console.log(`profile ready: ${profile.displayName}`)) +) + +Effect.runPromise(program) +// Output: +// user-1 cache lookup 1: Missing +// user-1 cache lookup 2: Missing +// user-1 cache lookup 3: Available +// profile ready: Ada +``` + +The first cache lookup runs immediately. If it returns `Missing`, the schedule +waits before the next lookup. The runnable example uses a short delay; a +production path can use a longer cadence. If the lookup returns `Available`, the +schedule stops and `Effect.repeat` returns that final `Available` value. + +The `Missing` branch after `Effect.repeat` is unreachable for this unbounded +schedule because the schedule stops only when the latest successful observation +is no longer missing. It becomes reachable when you add a limit. + +##### Variants + +Add a recurrence cap when the caller should stop waiting after a bounded number +of misses. With the cap, `Effect.repeat` can return `Missing` because the +recurrence limit may stop the schedule before the cache entry appears. Interpret +that result explicitly instead of assuming the data was found. + +Add `Schedule.jittered` when many callers may wait for the same key. It changes +the timing of each recurrence, not the stop condition. + +##### Notes and caveats + +Use `Effect.repeat` here because the decision is based on successful lookup +results. `Effect.retry` feeds failures into the schedule, which is the wrong +shape when "missing" is ordinary data. + +The schedule does not delay the first lookup. It controls only recurrences after +the first successful lookup. + +Keep the lookup effect responsible for classification. Translate only expected +absence into `Missing`; leave real lookup failures in the error channel. + +#### 22.4 Stop when a value stabilizes + +Some workflows do not have a terminal status field. Instead, they are finished +when repeated observations stop changing. A read model may be caught up when two +consecutive reads report the same version, a cache may be warm when its checksum +stops changing, or a derived aggregate may be ready when both its revision and +item count stay the same. + +This recipe uses a schedule over successful outputs. The effect performs the +observation. The schedule remembers enough previous output to decide whether the +next successful output is stable. + +##### Problem + +A projection reader may need two consecutive snapshots with the same version and +item count before treating the projection as settled. + +That comparison should be visible in the schedule. Future readers should not have +to infer "stable" from an unstructured loop, a mutable variable outside the +policy, or scattered sleep calls. + +##### When to use it + +Use this when each successful run is an observation and completion means "the +observed value has stopped changing". + +Good examples include polling a projection version, waiting for an eventually +consistent count to settle, or reading a checksum until two consecutive reads +match. In each case, define exactly what equality means for the workflow. + +##### When not to use it + +Do not use this to retry failures. `Effect.repeat` feeds successful values into +the schedule; a failure stops the repetition with that failure unless you handle +it separately. + +Do not use a single unchanged observation when the domain can pause and then +continue changing. In that case, require a longer stable streak, add a time +budget, or wait for a stronger terminal signal. + +Avoid vague comparisons. For numeric values, exact equality may be too strict or +too weak. Prefer a named predicate such as "within tolerance" when that is the +real business rule. + +##### Schedule shape + +Start with a schedule whose input and output are the successful observation, +reduce those observations into stability state, and continue only while that +state is not stable. `Schedule.identity()` passes each successful +`Snapshot` through as the schedule output. `Schedule.reduce` keeps the previous +observation in schedule state. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +interface Snapshot { + readonly version: string + readonly itemCount: number +} + +interface StabilityState { + readonly previous: Snapshot | undefined + readonly current: Snapshot | undefined + readonly stable: boolean +} + +const snapshots: ReadonlyArray = [ + { version: "v1", itemCount: 8 }, + { version: "v2", itemCount: 10 }, + { version: "v2", itemCount: 10 } +] + +let reads = 0 + +const readSnapshot: Effect.Effect = Effect.gen(function*() { + const index = yield* Effect.sync(() => { + const current = reads + reads += 1 + return current + }) + const snapshot = snapshots[index] ?? snapshots[snapshots.length - 1]! + + yield* Console.log( + `snapshot ${index + 1}: version=${snapshot.version}, items=${snapshot.itemCount}` + ) + return snapshot +}) + +const sameSnapshot = (left: Snapshot, right: Snapshot) => + left.version === right.version && left.itemCount === right.itemCount + +const initialState: StabilityState = { + previous: undefined, + current: undefined, + stable: false +} + +const untilStable = Schedule.identity().pipe( + Schedule.bothLeft(Schedule.spaced("100 millis")), + Schedule.reduce( + () => initialState, + (state, current): StabilityState => ({ + previous: state.current, + current, + stable: state.current !== undefined && sameSnapshot(state.current, current) + }) + ), + Schedule.while(({ output }) => !output.stable) +) + +const program = readSnapshot.pipe( + Effect.repeat(untilStable), + Effect.flatMap((state) => + Console.log( + `stable at version ${state.current?.version} with ${state.current?.itemCount} items` + ) + ) +) + +Effect.runPromise(program) +// Output: +// snapshot 1: version=v1, items=8 +// snapshot 2: version=v2, items=10 +// snapshot 3: version=v2, items=10 +// stable at version v2 with 10 items +``` + +`readSnapshot` runs once before the schedule is consulted. The first successful +snapshot cannot be stable because there is no previous snapshot to compare with. +After each later success, the schedule compares the latest snapshot with the +previous one. When `sameSnapshot` returns `true`, `Schedule.while` returns +`false`, and repetition stops. + +The repeat returns the final `StabilityState`. Its `current` field is the +snapshot that matched `previous`. + +##### Variants + +For domains that can pause and then continue changing, require a longer stable +streak instead of one unchanged comparison. Track a count in the reduced state +and stop only after the count reaches the required number of unchanged +observations. + +Add a recurrence limit or elapsed budget when stabilization is not guaranteed. +If the limit stops the schedule first, inspect the final state and return a +domain-specific "not stable yet" result. + +##### Notes and caveats + +This is an output condition, so it belongs with `Effect.repeat`, not +`Effect.retry`. Retry schedules observe failures. Repeat schedules observe +successful values. + +The schedule does not delay the first observation. Delays apply only before +later recurrences. + +If the value never stabilizes, an unbounded stability schedule can repeat +forever. Add a count limit, a time budget, or external cancellation unless the +surrounding workflow already provides one. + +#### 22.5 Stop on fatal errors + +Fatal errors should bypass retry timing. Classify raw failures into domain +errors first, then let only recoverable failures reach the retry schedule. + +##### Problem + +One operation can fail for temporary reasons, such as a timeout or overloaded +dependency, or for fatal reasons, such as bad input or missing authorization. +The retry budget should be spent only on failures that may recover without +changing the request. + +##### When to use it + +Use this recipe when a single operation can produce both retryable and +non-retryable failures. It fits HTTP clients, database calls, message +publication, and worker steps where a timeout may recover but validation or +authorization should stop immediately. + +The classification belongs next to the boundary that understands the failure. +For example, translate HTTP `408`, `429`, and `503` into transient domain errors, +and translate HTTP `400`, `401`, and `403` into fatal domain errors before the +retry boundary. + +##### When not to use it + +Do not ask a schedule to discover whether an error is fatal. If the error is +known to be permanent, classify it before retrying and let it bypass the +schedule. + +Also avoid retrying non-idempotent writes unless the operation has a clear +deduplication or transaction guarantee. Idempotent means safe to run more than +once with the same effect; a retry policy does not provide that guarantee. + +##### Schedule shape + +Use a normal timing schedule for retryable failures, then add the retry gate at +the `Effect.retry` call site: + +- `schedule` controls delay and retry count +- `while` decides whether the current typed failure is retryable + +This keeps the responsibilities separate. The schedule answers "when and how +many times?", while the predicate answers "is this failure retryable?" + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class TransientDownstreamError extends Data.TaggedError("TransientDownstreamError")<{ + readonly reason: "Timeout" | "Unavailable" | "RateLimited" +}> {} + +class FatalDownstreamError extends Data.TaggedError("FatalDownstreamError")<{ + readonly reason: "BadRequest" | "Unauthorized" | "Forbidden" +}> {} + +type DownstreamError = TransientDownstreamError | FatalDownstreamError + +let attempts = 0 + +const callDownstream = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`attempt ${attempts}`) + + if (attempts === 1) { + return yield* Effect.fail( + new TransientDownstreamError({ reason: "Timeout" }) + ) + } + + return yield* Effect.fail( + new FatalDownstreamError({ reason: "Unauthorized" }) + ) +}) + +const isTransient = (error: DownstreamError): error is TransientDownstreamError => + error._tag === "TransientDownstreamError" + +const retryPolicy = Schedule.exponential("20 millis").pipe( + Schedule.both(Schedule.recurs(5)) +) + +const program = callDownstream.pipe( + Effect.retry({ + schedule: retryPolicy, + while: isTransient + }), + Effect.matchEffect({ + onFailure: (error) => + Console.log( + `stopped on ${error._tag}/${error.reason} after ${attempts} attempts` + ), + onSuccess: (value) => Console.log(`succeeded with ${value}`) + }) +) + +Effect.runPromise(program) +// Output: +// attempt 1 +// attempt 2 +// stopped on FatalDownstreamError/Unauthorized after 2 attempts +``` + +##### Variants + +For a user-facing request, keep the retry budget small or add an elapsed budget +with `Schedule.during` so callers get a prompt answer. + +For a background worker, use a larger budget and add logging or metrics around +classification. The useful signal is often "fatal error bypassed retry", not +just "retry exhausted". + +If the downstream error carries retry metadata, classify that data before retry +as well. For example, a rate-limit response can become a transient error with a +parsed delay, and a custom schedule can use that delay without inspecting raw +HTTP headers elsewhere. + +##### Notes and caveats + +`Schedule.recurs(5)` means at most five retries after the original attempt. The +first call is not counted as a schedule recurrence. + +`Effect.retry` observes failures, not successes. If `callDownstream` fails with +`FatalDownstreamError`, `while: isTransient` rejects it and the program fails +with that fatal error immediately. If it fails with `TransientDownstreamError`, +the schedule decides the next delay until the retry budget is exhausted. + +Keep fatal and transient error types separate when possible. A single loose +error type with a boolean flag tends to spread retry decisions through the code +base. Tagged domain errors make the stop condition explicit at the retry +boundary. + +#### 22.6 Classify errors before retrying + +Retry policies should be narrow. A schedule can say when to try again and when +to stop, but it should not be asked to make every domain decision. + +##### Problem + +A downstream call can fail for several reasons. Some failures are temporary: +timeouts, overload, rate limits, or a service that is briefly unavailable. Other +failures are final for the current request: bad input, authorization failure, +missing configuration, or a business rule violation. + +Classify the typed failure first, then let `Effect.retry` apply the schedule +only to genuinely transient failures. Using one broad retry policy for all +errors delays permanent failures, adds load, and hides whether the operation was +never retryable or merely exhausted its retry budget. + +##### When to use it + +Use this pattern when the same effect can fail with both transient and +non-transient typed errors. It is a good fit for HTTP clients, database calls, +message brokers, cloud control planes, and service-to-service requests where a +small set of failures should be attempted again. + +Keep the classification close to the effect that knows the domain. A predicate +such as `isTransient` is easier to review than a schedule that silently retries +every error it receives. + +##### When not to use it + +Do not use this to make unsafe work safe to retry. A non-idempotent write still +needs an idempotency key, transaction boundary, deduplication strategy, or +another domain guarantee. + +Do not retry validation errors, authentication errors, authorization errors, +malformed requests, or configuration errors. Those failures should return +immediately so the caller can fix the request or escalate the operational issue. + +##### Schedule shape + +Use two separate pieces: + +- a predicate that decides whether the typed failure is transient +- a bounded schedule that decides retry timing and termination + +`Schedule.exponential("100 millis")` provides the backoff curve. By itself, it +is unbounded. `Schedule.recurs(4)` adds a maximum of four retries after the +original attempt. `Schedule.jittered` spreads retry attempts around the +exponential delay so multiple callers do not retry together. + +With `Effect.retry({ schedule, while })`, the `while` predicate is checked for +the typed failure. If it returns `false`, retrying stops immediately and that +failure is returned. If it returns `true`, the failure is fed to the schedule, +which decides whether another retry is allowed and how long to wait. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class DownstreamError extends Data.TaggedError("DownstreamError")<{ + readonly reason: + | "Timeout" + | "Unavailable" + | "RateLimited" + | "BadRequest" + | "Unauthorized" +}> {} + +const classifyStatus = (status: number): DownstreamError => { + if (status === 408) { + return new DownstreamError({ reason: "Timeout" }) + } + if (status === 429) { + return new DownstreamError({ reason: "RateLimited" }) + } + if (status >= 500) { + return new DownstreamError({ reason: "Unavailable" }) + } + if (status === 401 || status === 403) { + return new DownstreamError({ reason: "Unauthorized" }) + } + return new DownstreamError({ reason: "BadRequest" }) +} + +const statuses: ReadonlyArray = [429, 401] +let attempts = 0 + +const callDownstream = Effect.gen(function*() { + attempts += 1 + const status = statuses[attempts - 1] ?? 200 + + yield* Console.log(`downstream returned ${status}`) + + if (status === 200) { + return "ok" + } + + return yield* Effect.fail(classifyStatus(status)) +}) + +const isTransient = (error: DownstreamError) => + error.reason === "Timeout" || + error.reason === "Unavailable" || + error.reason === "RateLimited" + +const retryTransientFailures = Schedule.exponential("100 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const program = callDownstream.pipe( + Effect.retry({ + schedule: retryTransientFailures, + while: isTransient + }), + Effect.matchEffect({ + onFailure: (error) => Console.log(`stopped on ${error.reason} after ${attempts} attempts`), + onSuccess: (value) => Console.log(`succeeded with ${value}`) + }) +) + +Effect.runPromise(program) +// Output: +// downstream returned 429 +// downstream returned 401 +// stopped on Unauthorized after 2 attempts +``` + +The `429` response is classified as transient and retried. The later `401` is +classified as `Unauthorized`, so retrying stops immediately and reports that +typed error. + +##### Variants + +Use a faster, smaller policy for user-facing paths so a permanent failure is not +hidden for long. Use an elapsed budget with `Schedule.during` when the caller +cares more about total waiting time than attempt count. + +The same `isTransient` predicate can be reused with either schedule. The +predicate answers "is this failure retryable?" The schedule answers "how should +retrying proceed?" + +##### Notes and caveats + +`Effect.retry` retries typed failures from the error channel. Defects and fiber +interruptions are not made retryable by a schedule. + +Prefer the `while` option on `Effect.retry` for error classification. It keeps +the domain predicate at the retry boundary and leaves `Schedule` responsible for +recurrence mechanics: delay, jitter, limits, and observation. + +`Schedule.while` is lower level. It receives schedule metadata, including the +input, output, attempt, and selected delay. Use it when a schedule itself must +stop based on schedule metadata. For ordinary error classification before +retrying, `Effect.retry({ while })` is clearer. + +### 23. Combine Limits and Delays + +#### 23.1 Retry 5 times with fixed spacing + +You want a failing effect to run once immediately, then retry at most five +times with the same delay before each retry. Compose the spacing and retry +limit so both concerns are visible at the retry boundary. + +##### Problem + +You need a bounded retry policy for a transient operation such as an inventory +lookup or startup check. The first attempt should happen right away. If it +fails, the next five retries should be spaced by a fixed interval. + +The policy should make the off-by-one rule clear: "retry five times" means one +original attempt plus up to five retries, for at most six total attempts. + +##### When to use it + +Use this recipe when the operation is safe to run again and a fixed pause is +enough recovery time. It fits idempotent HTTP requests, short dependency +outages, service startup checks, and reconnect attempts where a steady cadence +is easier to reason about than backoff. + +It is also useful when logs and runbooks need a simple answer: the call is tried +once, then retried up to five more times at the chosen spacing. + +##### When not to use it + +Do not use retries to hide permanent failures. Bad input, invalid credentials, +authorization failures, and unsafe non-idempotent writes should be classified +before the retry policy is applied. + +Do not use a fixed spacing policy for overloaded or rate-limited dependencies +that need callers to spread out over time. Those cases usually call for +exponential backoff, jitter, server-provided retry metadata, or a time budget. + +Do not use `Schedule.recurs(5)` when the requirement is five total attempts. In +that case the first attempt counts too, so the retry limit would be +`Schedule.recurs(4)`. + +##### Schedule shape + +Start with `Schedule.spaced` for the cadence, then add `Schedule.recurs(5)` as +the count guard. Combining them with `Schedule.both` means both schedules must +continue, so the policy stops when the retry count is exhausted. + +With `Effect.retry`, the first execution is not scheduled. It runs immediately. +Only failures after that first execution are fed to the schedule: + +- attempt 1: run immediately +- if attempt 1 fails: wait 1 second +- attempt 2: retry 1 +- if attempt 2 fails: wait 1 second +- continue through retry 5 +- if retry 5 fails: propagate the last typed failure + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{ + readonly service: string +}> {} + +let attempts = 0 + +const fetchInventory = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`inventory attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail( + new ServiceUnavailable({ service: "inventory" }) + ) + } + + return ["sku-123", "sku-456"] as const +}) + +const retry5TimesWithFixedSpacing = Schedule.spaced("20 millis").pipe( + Schedule.both(Schedule.recurs(5)) +) + +const program = fetchInventory.pipe( + Effect.retry(retry5TimesWithFixedSpacing), + Effect.matchEffect({ + onFailure: (error) => Console.log(`failed with ${error._tag} after ${attempts} attempts`), + onSuccess: (items) => Console.log(`loaded ${items.length} items after ${attempts} attempts`) + }) +) + +Effect.runPromise(program) +// Output: +// inventory attempt 1 +// inventory attempt 2 +// inventory attempt 3 +// loaded 2 items after 3 attempts +``` + +The example uses `20 millis` so it terminates quickly. Use the same shape with +`1 second`, or any other fixed interval, in application code. + +##### Variants + +If you do not need to keep the output from `Schedule.recurs`, `Schedule.take(5)` +can express the same retry cap directly on the fixed-spacing schedule. For +`Effect.retry`, `take(5)` still means up to five retries after the original +attempt because schedule outputs correspond to scheduled retries. + +Use a named count guard when the retry limit is important enough to read as its +own policy. If the requirement is "try the operation five times total", allow +only four retries with `Schedule.recurs(4)`. + +##### Notes and caveats + +`Effect.retry` feeds typed failures into the schedule. It does not retry defects +or fiber interruptions as typed failures. + +`Schedule.spaced("1 second")` delays retries; it does not delay the first +attempt. The delay happens before each retry begins. + +`Schedule.recurs(n)` counts scheduled recurrences, not total executions. With +`Effect.retry`, a recurrence is a retry. With `Effect.repeat`, a recurrence is a +repeat after a successful original execution. + +#### 23.2 Retry 5 times with exponential backoff + +Exponential backoff is a good default when a failure may be temporary but +retrying immediately would add pressure to the dependency. The retry limit is +what makes that policy operationally bounded. + +##### Problem + +You call a dependency that sometimes fails with a transient error. The operation +is safe to retry, but it should not retry forever and it should not hammer the +dependency while it is unhealthy. + +You want the policy to say three things clearly: + +- run the original attempt immediately +- after each failure, wait with exponential backoff +- stop after five scheduled retries + +##### When to use it + +Use this recipe for idempotent work where a later attempt can reasonably +succeed: reading from an overloaded service, refreshing cached metadata, +submitting a deduplicated event, or calling an internal API during a short +deploy window. + +It is especially useful when code reviewers and operators need an exact answer +to "how many times can this run?" With `Schedule.recurs(5)`, the answer is one +original attempt plus at most five retries. + +##### When not to use it + +Do not use backoff to hide permanent failures. Bad input, forbidden access, +missing credentials, nonexistent resources, and schema errors should fail +without retrying. + +Do not retry unsafe writes unless the operation has an idempotency key, +transaction boundary, or another guarantee that repeated execution cannot +duplicate the side effect. + +Do not treat a retry count as a latency budget. Five retries can still take too +long if each attempt blocks before failing. If callers need a hard elapsed-time +limit, add `Schedule.during` or put an explicit timeout around the operation. + +##### Schedule shape + +Start with the delay shape, then add the retry limit. +`Schedule.exponential("200 millis")` starts with a 200 millisecond delay and, +with the default factor, doubles the delay on later recurrences. + +`Schedule.recurs(5)` allows five scheduled recurrences. With `Effect.retry`, +those recurrences are retries after failures. `Schedule.both` requires both +schedules to continue, so the combined policy stops when the retry count is +exhausted. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type TransientError = { + readonly _tag: "Timeout" | "Unavailable" | "RateLimited" +} + +let attempts = 0 + +const callDownstream = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`downstream attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail({ + _tag: attempts === 1 ? "Timeout" : "Unavailable" + } as TransientError) + } + + return "response body" +}) + +const retryPolicy = Schedule.exponential("20 millis").pipe( + Schedule.both(Schedule.recurs(5)) +) + +const program = callDownstream.pipe( + Effect.retry(retryPolicy), + Effect.matchEffect({ + onFailure: (error) => Console.log(`failed with ${error._tag} after ${attempts} attempts`), + onSuccess: (value) => Console.log(`succeeded with "${value}" after ${attempts} attempts`) + }) +) + +Effect.runPromise(program) +// Output: +// downstream attempt 1 +// downstream attempt 2 +// downstream attempt 3 +// downstream attempt 4 +// succeeded with "response body" after 4 attempts +``` + +The example uses a `20 millis` base interval so it terminates quickly. With this +policy, `callDownstream` can run at most six times total: one original attempt +plus five retries. + +##### Variants + +Use a larger base interval when the dependency needs more time to recover. + +Use a smaller retry limit for user-facing requests where returning a clear error +quickly matters more than exhausting every recovery chance. + +For fleet-wide retries, add jitter after the exponential cadence so identical +clients do not retry in lockstep. + +##### Notes and caveats + +`Schedule.exponential` is unbounded on its own. Always combine it with a retry +limit, elapsed-time budget, predicate, or another stopping condition for +request/response work. + +`Schedule.recurs(5)` counts retries, not total executions. If a requirement says +"try five times total", use `Schedule.recurs(4)`. + +`Effect.retry` retries typed failures from the error channel. Defects and fiber +interruptions are not retried as ordinary typed failures. + +#### 23.3 Retry 10 times with jittered backoff + +Use this policy when a transient failure should get several chances to recover +without every caller retrying at the same moments. + +##### Problem + +You have an effect that may fail because a dependency is restarting, +overloaded, briefly unreachable, or returning a retryable service error. A plain +`Schedule.exponential` retry policy backs off over time, but it is unbounded by +itself. If many workers use the same deterministic backoff, they can also retry +at the same boundaries and create bursts. + +You want the operation to retry at most ten times after the original attempt, +with exponential delays that are randomly adjusted around each computed delay. + +##### When to use it + +Use this recipe for retryable, idempotent work that crosses a process or network +boundary: service calls, queue operations, cache fetches, database reconnects, +or client initialization. Ten retries is enough to ride out many short +incidents while still making exhaustion explicit. + +It is especially useful when the same retry policy can run across many fibers, +workers, pods, or service instances. Jitter spreads retry traffic so fleet-wide +load is less likely to arrive as one coordinated spike. + +##### When not to use it + +Do not use this policy for permanent failures such as validation errors, +authorization failures, malformed requests, or missing configuration. Classify +those errors before retrying. + +Do not use it to make unsafe writes safe. Retried writes still need idempotency, +deduplication, transactions, or another domain guarantee that repeated +execution is acceptable. + +Do not use ten retries as a default latency budget for interactive paths. A +user-facing request may need fewer retries, a smaller elapsed-time bound, or a +fallback once the dependency is still unavailable. + +##### Schedule shape + +`Schedule.exponential("200 millis")` starts with a 200 millisecond delay and +doubles the base delay after each failed attempt. + +`Schedule.jittered` modifies each recurrence delay between 80% and 120% of the +delay chosen by the schedule it wraps. With a 200 millisecond base, the first +retry waits somewhere from 160 to 240 milliseconds, the next retry is jittered +around 400 milliseconds, and so on. + +`Schedule.both(Schedule.recurs(10))` adds the stopping condition. Both sides of +the composed schedule must continue, so the exponential schedule supplies the +delay while `Schedule.recurs(10)` supplies the retry limit. + +With `Effect.retry`, the original effect runs immediately. `Schedule.recurs(10)` +means ten retries after that original execution, for up to eleven executions in +total. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{ + readonly status: number +}> {} + +const statuses = [503, 503, 200] as const +let attempts = 0 + +const callService = Effect.gen(function*() { + attempts += 1 + const status = statuses[attempts - 1] ?? 200 + + yield* Console.log(`service attempt ${attempts}: ${status}`) + + if (status === 200) { + return "ok" + } + + return yield* Effect.fail(new ServiceUnavailable({ status })) +}) + +const retryTenTimesWithJitteredBackoff = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(10)) +) + +const program = callService.pipe( + Effect.retry({ + schedule: retryTenTimesWithJitteredBackoff, + while: (error) => error.status === 429 || error.status >= 500 + }), + Effect.matchEffect({ + onFailure: (error) => Console.log(`failed with HTTP ${error.status} after ${attempts} attempts`), + onSuccess: (value) => Console.log(`succeeded with ${value} after ${attempts} attempts`) + }) +) + +Effect.runPromise(program) +// Output: +// service attempt 1: 503 +// service attempt 2: 503 +// service attempt 3: 200 +// succeeded with ok after 3 attempts +``` + +The example uses a `10 millis` base interval so it terminates quickly. The +`while` predicate keeps non-retryable typed failures out of the schedule. If all +ten retries fail, `program` fails with the last `ServiceUnavailable`. + +##### Variants + +Use a smaller retry budget when the caller needs a quick answer. Use a larger +starting delay when the dependency is already under pressure. + +If the operation has a hard elapsed-time budget, add a time limit alongside the +attempt limit instead of relying on retry count alone. + +##### Notes and caveats + +`Schedule.exponential` is unbounded by itself. Pair it with an attempt limit, +elapsed-time limit, predicate, or another stopping condition before using it as +a production retry policy. + +`Schedule.jittered` changes timing only. It does not reduce the number of +callers that may retry, and it does not decide which failures are safe to retry. +Use admission control, concurrency limits, circuit breakers, or load shedding +when the fleet can still produce more retry traffic than the dependency can +handle. + +The composed schedule output is a pair of outputs from the jittered exponential +schedule and the recurrence counter. Plain `Effect.retry` uses the schedule for +retry decisions and returns the retried effect's successful value, so that +nested output usually does not appear in application code. + +#### 23.4 Poll with both interval and deadline + +Polling usually needs two separate limits. The interval controls load on the +remote system. The deadline controls how long the caller is willing to keep +observing a non-terminal state. Model those as two schedules and combine them, +instead of hiding a sleep and a clock check inside a loop. + +##### Problem + +You need to poll a job, export, provisioning, payment, or deployment status +endpoint every few seconds, but only until either the work reaches a terminal +state or the polling window expires. + +The first status read should happen immediately. After each successful +non-terminal read, wait for the interval before checking again. If the elapsed +recurrence budget is exhausted first, return the last observed status so the +caller can decide whether to report a timeout, keep tracking in the background, +or surface the last known state. + +##### When to use it + +Use this for job, export, provisioning, indexing, payment, or deployment status +polling where a `"running"` response is a successful observation, not an +exceptional failure. + +It is also a good fit when operators need to answer both questions separately: +"How often do we call the status endpoint?" and "When do we stop waiting?" + +##### When not to use it + +Do not use this as a retry policy for a failing status endpoint. With +`Effect.repeat`, successful status values feed the schedule; a failure from the +status read stops the repeat. Add a separate retry around the read itself if +transient transport failures should be retried. + +Do not treat `Schedule.during` as a hard interruption timeout for an in-flight +request. The deadline is checked at recurrence decision points after successful +observations. Use `Effect.timeout` on the status read when each request needs +its own hard deadline. + +##### Schedule shape + +Use `Schedule.spaced` for the gap after each successful status read, and +`Schedule.during` for the elapsed recurrence budget. + +`Schedule.passthrough` makes the latest successful status the schedule output. +That lets `Schedule.while` express terminal-state detection directly against +the observed status. `Schedule.bothLeft` keeps that status as the output while +requiring both the cadence policy and the deadline policy to allow another +recurrence. + +##### Example + +```ts +import { Console, Effect, Fiber, Schedule } from "effect" +import { TestClock } from "effect/testing" + +type JobStatus = + | { readonly _tag: "Running"; readonly jobId: string } + | { readonly _tag: "Completed"; readonly jobId: string; readonly resultId: string } + | { readonly _tag: "Failed"; readonly jobId: string; readonly reason: string } + +type StatusReadError = { readonly _tag: "StatusReadError" } + +type PollDeadlineExceeded = { + readonly _tag: "PollDeadlineExceeded" + readonly lastStatus: JobStatus +} + +let reads = 0 + +const readStatus = Effect.fnUntraced(function*(jobId: string) { + reads += 1 + + const status: JobStatus = reads < 3 + ? { _tag: "Running", jobId } + : { _tag: "Completed", jobId, resultId: "result-1" } + + yield* Console.log(`read ${reads}: ${status._tag}`) + return status +}) + +const cadence = Schedule.spaced("5 seconds").pipe( + Schedule.setInputType(), + Schedule.passthrough +) + +const deadline = Schedule.during("2 minutes").pipe( + Schedule.setInputType() +) + +const pollEvery5SecondsForUpTo2Minutes = cadence.pipe( + Schedule.while(({ output }) => output._tag === "Running"), + Schedule.bothLeft(deadline) +) + +const pollJob = Effect.fnUntraced(function*(jobId: string) { + const status = yield* readStatus(jobId).pipe( + Effect.repeat(pollEvery5SecondsForUpTo2Minutes) + ) + + if (status._tag === "Running") { + return yield* Effect.fail( + { + _tag: "PollDeadlineExceeded", + lastStatus: status + } satisfies PollDeadlineExceeded + ) + } + + return status +}) + +const program = Effect.gen(function*() { + const fiber = yield* pollJob("job-1").pipe(Effect.forkDetach) + yield* TestClock.adjust("10 seconds") + + const status = yield* Fiber.join(fiber) + yield* Console.log(`poll result: ${status._tag}`) +}).pipe( + Effect.matchEffect({ + onFailure: (error: StatusReadError | PollDeadlineExceeded) => Console.log(`poll failed with ${error._tag}`), + onSuccess: () => Console.log("polling finished") + }), + Effect.provide(TestClock.layer()), + Effect.scoped +) + +Effect.runPromise(program) +``` + +`pollJob` performs the first read immediately. The next two reads are driven by +the five-second cadence, but `TestClock` advances those intervals instantly for +the runnable example. + +The final `PollDeadlineExceeded` branch is optional but often useful. Without +it, the repeat returns the last observed `JobStatus`, which may still be +`Running` when the deadline stops the schedule. + +##### Variants + +For a user-facing request, use a shorter deadline and return +`PollDeadlineExceeded` with the last known status so the UI can show progress +without pretending the job failed. + +For a background worker, increase the spacing and keep the same terminal-state +detection. If many workers start at the same time, apply `Schedule.jittered` to +the cadence after choosing the base interval. + +If each status request also needs a per-request timeout, put `Effect.timeout` on +`readStatus`. That timeout changes the behavior of an individual status read. +The schedule still controls only the recurrence interval, deadline, and +terminal-state detection. + +##### Notes and caveats + +`Schedule.spaced("5 seconds")` waits five seconds after a successful status read +before the next recurrence. Use `Schedule.fixed` instead when you need +wall-clock-aligned polling boundaries. + +`Schedule.during("2 minutes")` measures elapsed schedule time and stops the +repeat when the recurrence window is no longer open. It does not cancel a +status read already in progress. + +`Schedule.bothLeft` has intersection semantics: both schedules must want to +recur. The combined delay is the maximum delay requested by the two schedules, +and the output kept by the repeat is the left schedule output, the latest +`JobStatus` in this recipe. + +#### 23.5 Exponential backoff plus time budget + +Combine a growing retry delay with an elapsed retry window when the caller cares +about bounded recovery time more than a fixed attempt count. + +Use `Schedule.exponential` for the delay curve and `Schedule.during` for the +elapsed budget. Combined with `Schedule.both`, both policies must allow another +retry. + +##### Problem + +You are calling a dependency that sometimes returns retryable failures during +deploys, restarts, or load spikes. The caller can wait through a short recovery +window, but retrying should slow down after repeated failures and stop when that +window is exhausted. + +You want one policy that makes both parts visible: + +- the delay grows after each failed attempt +- the whole retry window has an elapsed-time budget +- the original attempt still runs immediately +- retrying stops when the budget is exhausted + +##### When to use it + +Use this recipe for idempotent dependency calls, startup checks, connection +setup, cache refresh, or background jobs where transient failure is expected but +unbounded retrying would create operational risk. + +It is a good fit when the requirement is phrased as a time window: "try for up +to 30 seconds" or "give the service a short recovery window." + +##### When not to use it + +Do not use a time budget to retry permanent failures. Bad input, invalid +credentials, forbidden access, malformed requests, and unsafe non-idempotent +writes should be filtered before this schedule is allowed to run. + +Do not treat `Schedule.during` as a timeout for an attempt that is already in +flight. A schedule is consulted between attempts; use an Effect timeout on the +attempt itself if one call needs a deadline. + +Do not use `Schedule.during` alone for production retries. It describes an +elapsed window, but it does not provide useful spacing. Pair it with a delay +schedule such as `Schedule.exponential`. + +##### Schedule shape + +`Schedule.exponential("200 millis")` starts with a 200 millisecond delay and +then multiplies each later delay by the default factor of `2`: 200ms, 400ms, +800ms, 1.6s, and so on. By itself, it keeps recurring forever. + +`Schedule.during("30 seconds")` keeps recurring while the schedule's elapsed +time is less than or equal to 30 seconds. It supplies the stopping window, not +the backoff cadence. + +`Schedule.both` combines the two schedules with "both must continue" semantics. +The exponential side contributes the delay and the `during` side contributes the +elapsed budget. When the budget closes, the combined schedule stops even if the +exponential side could keep going. + +This is different from an attempt count. A count limit such as +`Schedule.recurs(5)` says how many retries may be scheduled after the original +attempt. A time budget says how long the retry window may remain open. Slow +failed attempts can consume the budget before many retries happen; fast failed +attempts may fit more retries into the same budget. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class DependencyError extends Data.TaggedError("DependencyError")<{ + readonly attempt: number +}> {} + +let attempts = 0 + +const callDependency = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`dependency attempt ${attempts}`) + return yield* Effect.fail(new DependencyError({ attempt: attempts })) +}) + +const retryWithinBudget = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.during("70 millis")) +) + +const program = callDependency.pipe( + Effect.retry(retryWithinBudget), + Effect.catch((error) => Console.log(`stopped after ${attempts} attempts; last error was attempt ${error.attempt}`)) +) + +Effect.runPromise(program) +// Output: +// dependency attempt 1 +// dependency attempt 2 +// dependency attempt 3 +// dependency attempt 4 +// stopped after 4 attempts; last error was attempt 4 +``` + +The first call is immediate. After each failure, the schedule waits with +exponential backoff while the elapsed budget remains open. When the budget is +exhausted, `Effect.retry` fails with the last error, which the example logs. + +In production, add error classification before or around this policy so only +retryable failures spend the budget. + +##### Variants + +Add an attempt cap with `Schedule.both(Schedule.recurs(n))` only when count is a +real secondary constraint. Both limits then apply: the elapsed window must still +be open, and no more than `n` retries may be scheduled after the original +attempt. + +Use a shorter budget and smaller base delay for interactive paths. Use a larger +base delay for background work that should be conservative with a shared +dependency. + +##### Notes and caveats + +`Effect.retry` feeds failures into the schedule. If only some failures are +retryable, classify them before the schedule spends more of the budget. + +`Schedule.during` measures the schedule's elapsed recurrence window. It is not +a hard wall-clock cancellation boundary for running work. If an individual +attempt also needs a deadline, apply a timeout to that effect separately. + +The exact number of retries is intentionally not fixed by this recipe. The +budget is the primary limit; the exponential cadence determines how quickly +the operation consumes that budget through waiting between failed attempts. + +#### 23.6 Retry with cap plus max attempts + +Capped backoff combines early pressure relief with a visible ceiling on retry +delay and retry count. + +This is a retry policy, so the first call still happens immediately. +`Schedule` controls only the decisions after a failure. + +##### Problem + +You call a dependency that may fail briefly during deploys, restarts, or load +spikes. Immediate retries create pressure, but pure exponential backoff can +eventually wait longer than the caller can tolerate. Reviewers should be able to +see both the maximum delay and the maximum number of follow-up attempts. + +You want a policy that: + +- starts with a small exponential delay +- never waits more than a fixed cap between retries +- stops after a fixed number of retry attempts +- makes the total number of executions obvious in code review + +##### When to use it + +Use this recipe for retryable, idempotent operations where a short recovery +window is useful: a control-plane request, a cache fill, a metadata fetch, or an +internal service call that sometimes returns a transient `5xx`. + +It is a good default when you need a clear ceiling. For example, +`Schedule.recurs(5)` means at most five retries after the original attempt, so +the effect can execute at most six times total. + +##### When not to use it + +Do not use capped backoff to retry permanent failures. Bad input, authorization +failures, missing resources, and unsafe non-idempotent writes should usually fail +without retrying. + +Also avoid treating the delay cap as a full request timeout. The schedule limits +the wait between retries. It does not interrupt one slow in-flight attempt. + +##### Schedule shape + +Start with `Schedule.exponential` for the growing delay curve. Use +`Schedule.modifyDelay` to replace any delay above the cap. Then combine the +capped delay schedule with `Schedule.recurs` so both constraints must +continue for another retry to happen. + +`Schedule.both` has intersection semantics: the combined schedule recurs only +while both schedules recur, and it uses the larger of their delays. Since +`Schedule.recurs(5)` has no meaningful delay of its own, the capped backoff side +provides the wait time and the recurrence side provides the retry count. + +##### Example + +```ts runnable deterministic +import { Console, Duration, Effect, Schedule } from "effect" + +type TransientError = { + readonly _tag: "TransientError" + readonly attempt: number +} + +let attempts = 0 + +const fetchMetadata = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`metadata attempt ${attempts}`) + return yield* Effect.fail({ _tag: "TransientError", attempt: attempts } satisfies TransientError) +}) + +const retryWithCappedBackoff = Schedule.exponential("10 millis").pipe( + Schedule.modifyDelay((_, delay) => { + const capped = Duration.min(delay, Duration.millis(40)) + return Console.log(`next delay: ${Duration.toMillis(capped)}ms`).pipe( + Effect.as(capped) + ) + }), + Schedule.both(Schedule.recurs(4)) +) + +const program = fetchMetadata.pipe( + Effect.retry(retryWithCappedBackoff), + Effect.catch((error) => Console.log(`gave up after ${attempts} attempts; last error was attempt ${error.attempt}`)) +) + +Effect.runPromise(program) +// Output: +// metadata attempt 1 +// next delay: 10ms +// metadata attempt 2 +// next delay: 20ms +// metadata attempt 3 +// next delay: 40ms +// metadata attempt 4 +// next delay: 40ms +// metadata attempt 5 +// next delay: 40ms +// gave up after 5 attempts; last error was attempt 5 +``` + +The retry delays grow until they reach the cap, and `Schedule.recurs(4)` allows +at most four retries after the original call. + +##### Variants + +If you want the count limit to read as "take this many outputs from the backoff +schedule", put `Schedule.take(n)` directly on the backoff schedule. Use +`Schedule.recurs` when you want the retry-count guard to stand out as a separate +policy. + +For a fleet of clients, add `Schedule.jittered` before the delay cap and keep +`Schedule.modifyDelay` after it, so randomization cannot push a computed delay +past the maximum. + +##### Notes and caveats + +`Effect.retry` feeds failures into the schedule. If only some errors are +retryable, classify them before applying the policy or add a schedule predicate +that stops on non-retryable failures. + +`Schedule.exponential` is unbounded by itself. Pair it with `Schedule.recurs`, +`Schedule.take`, `Schedule.during`, or a domain-specific stop condition whenever +the policy can run in production. + +### 24. Multi-Phase Policies + +#### 24.1 Aggressive at startup, relaxed afterward + +Some startup workflows benefit from a short fast phase before settling into a +calmer cadence. Model that handoff as two named schedules sequenced with +`Schedule.andThen`. + +##### Problem + +You need a readiness probe that catches the quick startup path without hammering +a service that takes longer to become ready. A single fast +`Schedule.spaced("100 millis")` policy is too noisy for a long startup, while a +single slow policy gives poor startup responsiveness. Scattered sleeps make the +transition hard to review. + +Use a bounded warm-up phase followed by a steady-state phase: after the first +observation, check quickly for a limited number of recurrences, then check less +often. + +##### When to use it + +Use this recipe for readiness checks, startup dependency probes, leader election +observation, background job startup, cache warm-up, and similar workflows where +early completion is common but longer startup is still valid. + +The key requirement is that both phases are operationally acceptable. The fast +phase should have a visible bound, and the relaxed phase should be slow enough +that it can continue for the expected startup window without creating avoidable +load. + +##### When not to use it + +Do not use this schedule to hide a failed startup. If the domain has a clear +terminal failure, stop on that value. If startup must fail after a known budget, +add `Schedule.during` or another explicit limit. + +Do not apply an aggressive warm-up phase to many instances without considering +coordination. If a whole fleet starts at once, a deterministic 100 millisecond +cadence can still synchronize callers. Add jitter where that matters. + +##### Schedule shape + +The phase boundary belongs in the schedule, not in a loop: + +1. `warmUp` is fast and finite. +2. `steadyState` is slower and may continue until the status or budget stops it. +3. `Schedule.andThen(warmUp, steadyState)` sequences the phases. +4. `Schedule.passthrough` lets the latest successful status decide whether to + continue. +5. `Schedule.while` stops when the status is no longer a startup state. + +The first effect run is not delayed. With `Effect.repeat`, the successful value +from each run is fed into the schedule. That is what allows the schedule to stop +when readiness is reached. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type Readiness = + | { readonly _tag: "Starting" } + | { readonly _tag: "Ready" } + | { readonly _tag: "Failed"; readonly reason: string } + +const observations: ReadonlyArray = [ + { _tag: "Starting" }, + { _tag: "Starting" }, + { _tag: "Starting" }, + { _tag: "Starting" }, + { _tag: "Ready" } +] + +let checks = 0 + +const checkReadiness = Effect.gen(function*() { + const status = observations[Math.min(checks, observations.length - 1)] + checks += 1 + yield* Console.log(`readiness check ${checks}: ${status._tag}`) + return status +}) + +const warmUp = Schedule.spaced("10 millis").pipe( + Schedule.take(3) +) + +const steadyState = Schedule.spaced("40 millis") + +const startupThenRelaxed = Schedule.andThen(warmUp, steadyState).pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input._tag === "Starting"), + Schedule.bothLeft( + Schedule.during("200 millis").pipe( + Schedule.satisfiesInputType() + ) + ) +) + +const program = Effect.repeat(checkReadiness, startupThenRelaxed).pipe( + Effect.flatMap((status) => Console.log(`finished with ${status._tag}`)) +) + +Effect.runPromise(program) +// Output: +// readiness check 1: Starting +// readiness check 2: Starting +// readiness check 3: Starting +// readiness check 4: Starting +// readiness check 5: Ready +// finished with Ready +``` + +`program` performs one readiness check immediately. If that check returns +`Starting`, the schedule allows another check after the warm-up delay. Once the +fast phase is exhausted, the policy switches to the slower phase. + +The repeat stops when `checkReadiness` returns `Ready` or `Failed`, because the +`Schedule.while` predicate only continues for `Starting`. The elapsed budget +prevents an indefinitely starting service from polling forever under this +workflow. + +##### Variants + +Use a smaller warm-up for user-facing paths, and a wider steady-state interval +for platform checks that can continue longer. For a fleet-wide startup policy, +add `Schedule.jittered` before the status predicate and elapsed budget. + +##### Notes and caveats + +`Schedule.andThen` is sequencing, not parallel composition. The second phase +does not participate until the first phase completes. + +Keep the warm-up phase finite. If the first phase is an unbounded schedule, the +relaxed phase will never run. + +`Schedule.take(20)` limits scheduled recurrences after the initial effect run. +It does not mean 20 total calls. + +`Schedule.while` sees schedule metadata. In this recipe the predicate checks +`metadata.input`, because `Effect.repeat` feeds the successful `Readiness` value +into the schedule after each check. + +The schedule controls the delay between checks. It does not time out an +individual readiness probe. If one probe can hang, apply a timeout to +`checkReadiness` itself before repeating it. + +#### 24.2 Fast checks during initialization + +Fast initialization checks are for dependencies that usually become ready +quickly. Keep the cadence and limits visible so the startup loop stays bounded. + +##### Problem + +At startup, database and broker checks may fail with `DependencyUnavailable` +while connections finish opening. Retry only that transient condition, and make +the policy answer three questions directly: + +- how long to wait between checks +- how many follow-up checks are allowed +- how much startup time the check may consume + +Without an explicit schedule, these rules tend to disappear into ad hoc sleeps +and counters. + +##### When to use it + +Use this recipe for initialization checks that are expected to settle quickly: +opening a connection pool, checking a local sidecar, validating that a required +topic exists, or confirming a warm cache is reachable. + +The check must be safe to run more than once. It should observe readiness or +perform idempotent setup, not repeat a write that could create duplicate work. + +##### When not to use it + +Do not use a fast startup schedule for steady-state monitoring. Once the +service is running, switch to a slower runtime schedule so health checks do not +create constant pressure. + +Do not retry permanent configuration failures. Missing credentials, malformed +connection strings, unsupported schema versions, and authorization failures +should fail startup immediately. + +Do not treat the schedule as a hard timeout for an individual check. +`Schedule.during("2 seconds")` is evaluated at recurrence decision points. Add +a timeout to the check itself if one probe must not run too long. + +##### Schedule shape + +Combine a fast cadence with a retryable-error predicate, a count limit, and a +short elapsed budget. + +`Schedule.spaced("100 millis")` waits briefly after each failed check. +`Schedule.while` prevents retries for permanent startup errors. +`Schedule.recurs(12)` allows at most twelve follow-up attempts. +`Schedule.during("2 seconds")` stops recurrence once the startup budget has +been used. + +The `both` combinator gives intersection semantics: the retry continues only +while all pieces of the policy still allow another recurrence. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type StartupCheckError = + | { readonly _tag: "DependencyUnavailable"; readonly dependency: string } + | { readonly _tag: "InvalidConfiguration"; readonly message: string } + +let databaseChecks = 0 + +const checkDatabase: Effect.Effect = Effect.gen(function*() { + databaseChecks += 1 + yield* Console.log(`database check ${databaseChecks}`) + if (databaseChecks < 3) { + return yield* Effect.fail( + { + _tag: "DependencyUnavailable", + dependency: "database" + } as const + ) + } +}) + +const checkMessageBroker: Effect.Effect = Console.log("broker check ok") + +const startupChecks = Effect.fnUntraced(function*() { + yield* checkDatabase + yield* checkMessageBroker +}) + +const fastInitializationChecks = Schedule.spaced("20 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.while(({ input }) => input._tag === "DependencyUnavailable"), + Schedule.both(Schedule.recurs(12)), + Schedule.both( + Schedule.during("200 millis").pipe( + Schedule.satisfiesInputType() + ) + ) +) + +const initialize = startupChecks().pipe( + Effect.retry(fastInitializationChecks) +) + +const program = Effect.gen(function*() { + yield* initialize + yield* Console.log("initialized") +}) + +Effect.runPromise(program) +// Output: +// database check 1 +// database check 2 +// database check 3 +// broker check ok +// initialized +``` + +`initialize` runs the first startup check immediately. If a dependency is not +available yet, it retries while the count and elapsed limits both still allow +another attempt. If the check fails with +`InvalidConfiguration`, the schedule stops and the original failure is returned. + +##### Variants + +For a purely local readiness check, reduce the delay and count, for example +`Schedule.spaced("25 millis").pipe(Schedule.both(Schedule.recurs(8)))`. +Keep the elapsed budget short so startup failure is reported quickly. + +For startup across many replicas, add `Schedule.jittered` after the cadence is +correct. Jitter spreads retries so a fleet does not hit the same dependency in +lockstep during a rollout. + +For checks that may hang, place `Effect.timeout` on the checked effect before +`Effect.retry`. The timeout bounds one probe; the schedule bounds the retry +window. + +##### Notes and caveats + +`Effect.retry` feeds failures into the schedule. That is why the error type is +made explicit with `Schedule.satisfiesInputType()` before +reading `metadata.input` in `Schedule.while`. + +The schedule does not delay the first check. It only decides whether to perform +another check after a failure. + +The elapsed budget is checked between attempts. Time spent inside each startup +check contributes to the elapsed schedule time before the next recurrence +decision, but the schedule does not interrupt an in-flight check. + +#### 24.3 Slow background cadence after readiness + +Readiness and monitoring are different phases: check quickly until the +dependency is ready, then observe at a slower cadence. `Schedule.andThen` makes +that phase transition explicit. + +##### Problem + +A worker cannot do useful work until a dependency returns `Ready`. Probing too +slowly delays useful work, but keeping the startup cadence after readiness only +creates noise and unnecessary load. + +The recurrence policy should make that operational intent visible: + +- fast checks while readiness is still pending +- a clear switch once `Ready` is observed +- slow, steady background monitoring afterward + +##### When to use it + +Use this recipe for service readiness checks, cache warm-up probes, +leader-election status checks, or control-plane watches where startup latency +matters but long-term polling pressure should stay low. + +It is especially useful when the same effect can be repeated in both phases: +first to discover readiness, then to continue observing the dependency at a +maintenance cadence. + +##### When not to use it + +Do not use this as a substitute for a real startup deadline. If the service must +fail fast when readiness never arrives, add an outer timeout or a separate +startup budget around the readiness workflow. + +Also avoid polling when the dependency can push a readiness signal, emit an +event, or complete a handshake directly. In those cases, a schedule may be +unnecessary background work. + +##### Schedule shape + +The startup phase uses `Schedule.spaced("250 millis")` so each +failed-to-be-ready observation is followed by a short pause. `Schedule.passthrough` +makes the successful value from the repeated effect available as the schedule +output, and `Schedule.while` stops the startup phase once that value is `Ready`. + +The steady-state phase uses a slower `Schedule.spaced("30 seconds")`. Because +it is sequenced with `Schedule.andThen`, it starts only after the startup phase +completes. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type Readiness = + | { readonly _tag: "Starting" } + | { readonly _tag: "Ready" } + +let probes = 0 + +const probeDependency = Effect.gen(function*() { + probes += 1 + const status: Readiness = probes < 3 + ? { _tag: "Starting" } + : { _tag: "Ready" } + yield* Console.log(`probe ${probes}: ${status._tag}`) + return status +}) + +const waitUntilReady = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input._tag !== "Ready") +) + +const backgroundCadence = Schedule.spaced("40 millis").pipe( + Schedule.take(3), + Schedule.satisfiesInputType() +) + +const readinessThenBackground = Schedule.andThen( + waitUntilReady, + backgroundCadence +) + +const program = Effect.repeat( + probeDependency, + readinessThenBackground +).pipe( + Effect.flatMap(() => Console.log("background monitoring sample finished")) +) + +Effect.runPromise(program) +// Output: +// probe 1: Starting +// probe 2: Starting +// probe 3: Ready +// probe 4: Ready +// probe 5: Ready +// probe 6: Ready +// background monitoring sample finished +``` + +The example bounds the background phase with `Schedule.take(3)` so it terminates +in `scratchpad/repro.ts`. A daemon would usually omit that bound and let scope +or supervision own the lifetime. + +##### Variants + +Use a shorter startup spacing when local readiness usually appears almost +immediately, and a longer spacing when the check itself is expensive. For +fleet-wide background monitoring, apply `Schedule.jittered` to the steady-state +cadence so ready instances do not all probe on the same boundary. + +If the monitoring must run on wall-clock intervals, use `Schedule.fixed` for +the background phase instead of `Schedule.spaced`. `Schedule.fixed` targets +interval boundaries; `Schedule.spaced` waits after each probe completes. + +##### Notes and caveats + +`Effect.repeat` feeds each successful `probeDependency` value into the schedule. +That is what lets `waitUntilReady` inspect `Readiness` and complete when it sees +`Ready`. + +The schedule does not make the first probe wait. The effect runs once, then the +schedule decides whether and when to run it again. After `Ready` is observed, +the sequenced schedule switches from startup responsiveness to slow background +cadence. + +#### 24.4 Immediate retries first, backoff later + +Some transient failures clear before a meaningful delay would help: a stale +pooled connection, a dependency that just became reachable, or a short +optimistic-concurrency conflict. A small immediate retry burst is reasonable +there, but only while the failure still looks brief. + +If the failure survives that burst, switch to backoff. `Schedule.andThen` +models that handoff directly: one schedule runs to completion, then the next +schedule starts. + +##### Problem + +Build a retry policy with two visible phases: a bounded zero-delay burst, then +a bounded exponential backoff. If both phases are exhausted, `Effect.retry` +returns the last typed failure. + +##### When to use it + +Use this when one or two instant retries are acceptable, but continued failure +means the dependency needs time. It fits idempotent reads, health checks, cache +refreshes, and small remote calls. Idempotent means repeating the operation has +the same externally visible result as running it once. + +##### When not to use it + +Do not use this for permanent failures such as validation errors, authorization +failures, malformed requests, missing configuration, or non-idempotent writes. +Classify those before applying the schedule. + +Do not make the immediate phase large. If you need many retries, start with +spacing or backoff instead. + +##### Schedule shape + +`Schedule.recurs(2)` allows two retry decisions after the original attempt. +`Schedule.exponential(...).pipe(Schedule.take(4))` allows four delayed retries. +Sequencing them with `Schedule.andThen` keeps the phase boundary reviewable. + +For `Effect.retry`, the original effect execution is not counted by the +schedule. The schedule starts only after a typed failure. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class GatewayError extends Data.TaggedError("GatewayError")<{ + readonly status: number + readonly message: string +}> {} + +let attempts = 0 + +const callGateway = Effect.gen(function*() { + attempts++ + yield* Console.log(`gateway attempt ${attempts}`) + + if (attempts <= 4) { + return yield* Effect.fail( + new GatewayError({ + status: 503, + message: `temporary failure ${attempts}` + }) + ) + } + + return `gateway succeeded on attempt ${attempts}` +}) + +const isRetryable = (error: GatewayError) => + error.status === 408 || + error.status === 429 || + error.status >= 500 + +const immediateRetries = Schedule.recurs(2) + +const delayedBackoff = Schedule.exponential("20 millis").pipe( + Schedule.take(4) +) + +const immediateThenBackoff = immediateRetries.pipe( + Schedule.andThen(delayedBackoff), + Schedule.satisfiesInputType(), + Schedule.while(({ input }) => isRetryable(input)) +) + +const program = callGateway.pipe( + Effect.retry(immediateThenBackoff), + Effect.flatMap((result) => Console.log(result)) +) + +Effect.runPromise(program) +// Output: +// gateway attempt 1 +// gateway attempt 2 +// gateway attempt 3 +// gateway attempt 4 +// gateway attempt 5 +// gateway succeeded on attempt 5 +``` + +The retry sequence is: + +- attempt 1: run `callGateway` +- retry 1: immediate, if the first attempt fails with a retryable `GatewayError` +- retry 2: immediate, if the second attempt fails with a retryable `GatewayError` +- retry 3: wait according to the first backoff delay +- retry 4 and later: continue the bounded backoff phase + +If all retry decisions are exhausted, `Effect.retry` returns the last typed +failure. If `isRetryable` returns `false`, the schedule stops immediately and +that failure is returned without entering the remaining phase. + +##### Variants + +For a user-facing request, reduce the backoff phase or add a short elapsed +budget with `Schedule.during`, so the caller gets a clear answer quickly. + +For a fleet-wide remote dependency, consider adding `Schedule.jittered` to the +backoff phase after the base cadence is correct. Jitter means randomizing each +delay slightly to avoid synchronized retries across many instances. It belongs +in the delayed phase; adding randomness to the immediate burst weakens the +"immediate first" contract. + +For startup checks, the immediate phase can be slightly larger when the +operation is local and cheap. Keep the backoff phase explicit so later startup +failure does not spin. + +##### Notes and caveats + +`Schedule.recurs(2)` means two retry decisions after the original attempt, not +two total executions. + +`Schedule.exponential(...)` recurs forever by itself, so the example uses +`Schedule.take(4)` to bound the delayed phase. + +`Schedule.andThen` is sequential composition, not parallel composition. Use it +when phase order is part of the policy. Use combinators such as `Schedule.both` +when two constraints should apply at the same time. + +#### 24.5 Fast polling first, slower polling later + +Some polling workflows need a brief responsive phase, then a calmer cadence. A +newly submitted export, payment, cache refresh, or provisioning request may +complete almost immediately. If it does not, polling every few hundred +milliseconds quickly becomes wasteful. + +Use `Schedule.andThen` to run the fast polling phase to completion, then switch +to the slower phase. + +##### Problem + +Model a status loop without scattering sleeps, counters, or phase flags through +the polling code. The first status read should happen immediately; the schedule +describes only follow-up reads and stop conditions. + +##### When to use it + +Use this when a workflow has two natural operational phases: + +- an early user-facing window where low latency matters +- a later background window where reducing load matters more + +This is a good fit for jobs that often finish in the first few seconds but may +occasionally take minutes, such as exports, media processing, payment +settlement, indexing, cache warmups, and cloud provisioning. + +##### When not to use it + +Do not use this when the remote system already provides a callback, queue +message, webhook, or subscription that can replace polling. + +Do not make the fast phase unbounded. Give it a small recurrence cap so a slow +workflow does not keep hammering the status endpoint. + +Do not use this schedule by itself to retry failed status reads. `Effect.repeat` +feeds successful values into the schedule. Transport failures, authorization +failures, and decoding failures remain in the effect failure channel and should +be classified separately if they need their own retry policy. + +##### Schedule shape + +Build each phase separately, then sequence them. `Schedule.passthrough` changes +the schedule output to the latest successful status value, so `Effect.repeat` +returns the last observation. `Schedule.while` stops as soon as a terminal +status is observed. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type Status = + | { readonly _tag: "Running"; readonly progress: number } + | { readonly _tag: "Completed"; readonly resultId: string } + | { readonly _tag: "Failed"; readonly reason: string } + +type StatusReadError = { + readonly _tag: "StatusReadError" + readonly message: string +} + +const observations: ReadonlyArray = [ + { _tag: "Running", progress: 10 }, + { _tag: "Running", progress: 35 }, + { _tag: "Running", progress: 70 }, + { _tag: "Completed", resultId: "export-123" } +] + +let reads = 0 + +const readStatus = (jobId: string): Effect.Effect => + Effect.gen(function*() { + const status = observations[Math.min(reads, observations.length - 1)] + reads++ + yield* Console.log(`${jobId}: read ${reads} -> ${status._tag}`) + return status + }) + +const fastPhase = Schedule.spaced("20 millis").pipe( + Schedule.take(3) +) + +const slowPhase = Schedule.spaced("60 millis").pipe( + Schedule.both(Schedule.during("500 millis")) +) + +const fastThenSlowPolling = Schedule.andThen(fastPhase, slowPhase).pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input._tag === "Running") +) + +export const pollJob = (jobId: string) => + readStatus(jobId).pipe( + Effect.repeat(fastThenSlowPolling) + ) + +const program = pollJob("job-1").pipe( + Effect.flatMap((status) => Console.log(`final status: ${status._tag}`)) +) + +Effect.runPromise(program) +// Output: +// job-1: read 1 -> Running +// job-1: read 2 -> Running +// job-1: read 3 -> Running +// job-1: read 4 -> Completed +// final status: Completed +``` + +`pollJob` reads the status immediately. If that first read is already +`"Completed"` or `"Failed"`, the repeat stops without waiting. If the job is +still `"Running"`, the schedule performs a short burst of fast spacing, then +moves to slower spacing. + +The returned effect succeeds with the latest observed `Status`. That value can +be terminal, or it can still be `"Running"` if the slow phase exhausts its +budget before completion. + +##### Variants + +Use fewer fast recurrences when the endpoint is expensive or globally rate +limited. For example, four recurrences at 500 milliseconds still gives a short +responsive window without producing as much request pressure. + +Use a longer slow interval for back-office workflows where completion can be +reported later. A 30 second or 1 minute slow phase is often more appropriate for +large exports, media processing, or asynchronous reconciliation. + +Add `Schedule.jittered` to the slow phase when many clients may start polling at +roughly the same time. Jitter is usually more important in the slow phase +because that phase contains the long-lived population of pollers. + +Use `Schedule.andThenResult` instead of `Schedule.andThen` when you need the +schedule output to preserve which phase produced it. For ordinary polling, the +phase is often less important than returning the latest observed status, so +`Schedule.passthrough` keeps the code simpler. + +##### Notes and caveats + +`Schedule.andThen` is phase sequencing, not intersection. The slow phase does +not start until the fast phase completes. + +`Schedule.spaced` waits after each successful status read completes. Use +`Schedule.fixed` only when the policy must target fixed wall-clock boundaries. + +The elapsed budget on `slowPhase` starts when that phase starts. If the whole +polling operation needs one overall deadline, combine the sequenced cadence with +a separate outer budget. + +When a schedule reads `metadata.input`, constrain the input type before +`Schedule.while`. In this recipe, `Schedule.satisfiesInputType()` makes +the successful status values visible to the predicate. + +#### 24.6 Phase-based control for long workflows + +Long-running workflows often need more than one recurrence shape. The first few +minutes may need frequent observations because users are waiting for visible +progress. After that, the workflow may still be healthy, but checking it too +often only adds load. Much later, the policy may become a watchdog: keep enough +visibility to notice completion or failure, but do not pretend the workflow is +still latency-sensitive. + +Model those phases as schedule values instead of encoding them with counters, +mutable phase flags, and scattered sleeps. Each phase can say how often it +recurs and when it is exhausted, and `Schedule.andThen` makes the handoff from +one phase to the next explicit. + +##### Problem + +Build a single polling schedule for follow-up status reads. The first status +read should happen immediately, so the schedule should describe only later reads +and their stopping conditions: + +- a responsive phase while fast completion is common +- a steady phase while the workflow is still expected to finish normally +- a watchdog phase for long tails +- an overall budget that stops the whole policy + +##### When to use it + +Use this recipe for workflows where operational expectations change over time: +exports, imports, media processing, indexing jobs, data backfills, provisioning +requests, settlement flows, and asynchronous reconciliations. + +It is especially useful when the same status endpoint serves both a user-visible +experience and a background monitoring path. The schedule keeps the early user +experience responsive without keeping the later background phase aggressive. + +##### When not to use it + +Do not poll when the producer can reliably notify you with a webhook, queue +message, subscription, or durable completion event. + +Do not use a long watchdog phase to hide a workflow that should have a real +deadline. If the business rule says the workflow must finish within 30 minutes, +make that deadline part of the workflow state or the outer effect, not just a +large polling schedule. + +Do not use this schedule to retry failed status reads. `Effect.repeat` feeds +successful status values into the schedule. Transport failures, decoding +failures, and authorization failures stay in the failure channel and need their +own retry or error handling policy if they are recoverable. + +##### Schedule shape + +Build the cadence from named phases and sequence them with `Schedule.andThen`. +The steady phase does not start until the responsive phase is exhausted, and the +watchdog phase does not start until the steady phase is exhausted. Then combine +that cadence with constraints that apply to the whole polling policy: + +- `Schedule.during("2 hours")` gives the whole schedule an elapsed-time budget. +- `Schedule.both` requires both the cadence and the budget to continue. +- `Schedule.passthrough` returns the latest successful workflow status. +- `Schedule.while` stops as soon as the workflow is no longer running. + +##### Example + +```ts +import { Console, Effect, Schedule } from "effect" + +type WorkflowStatus = + | { + readonly _tag: "Running" + readonly phase: "Queued" | "Processing" | "Finalizing" + readonly progress: number + } + | { readonly _tag: "Completed"; readonly artifactId: string } + | { readonly _tag: "Failed"; readonly reason: string } + +type StatusReadError = { + readonly _tag: "StatusReadError" + readonly message: string +} + +const observations: ReadonlyArray = [ + { _tag: "Running", phase: "Queued", progress: 0 }, + { _tag: "Running", phase: "Processing", progress: 25 }, + { _tag: "Running", phase: "Processing", progress: 60 }, + { _tag: "Running", phase: "Finalizing", progress: 90 }, + { _tag: "Completed", artifactId: "artifact-123" } +] + +let reads = 0 + +const readWorkflowStatus = ( + workflowId: string +): Effect.Effect => + Effect.gen(function*() { + const status = observations[Math.min(reads, observations.length - 1)] + reads++ + yield* Console.log(`${workflowId}: observation ${reads} -> ${status._tag}`) + return status + }) + +const responsivePhase = Schedule.spaced("20 millis").pipe( + Schedule.take(2) +) + +const steadyPhase = Schedule.spaced("50 millis").pipe( + Schedule.jittered, + Schedule.take(2) +) + +const watchdogPhase = Schedule.spaced("100 millis").pipe( + Schedule.jittered, + Schedule.take(2) +) + +const phasedCadence = responsivePhase.pipe( + Schedule.andThen(steadyPhase), + Schedule.andThen(watchdogPhase) +) + +const longWorkflowPolicy = phasedCadence.pipe( + Schedule.both(Schedule.during("1 second")), + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input._tag === "Running") +) + +export const pollWorkflow = (workflowId: string) => + readWorkflowStatus(workflowId).pipe( + Effect.repeat(longWorkflowPolicy) + ) + +const program = pollWorkflow("workflow-1").pipe( + Effect.flatMap((status) => Console.log(`final workflow status: ${status._tag}`)) +) + +Effect.runPromise(program) +``` + +`pollWorkflow` reads once immediately. If that first read returns +`"Completed"` or `"Failed"`, the repeat stops without waiting. If the workflow +is still `"Running"`, the schedule starts with responsive spacing, moves to a +steady cadence, then moves to watchdog checks. + +The effect succeeds with the latest observed `WorkflowStatus`. That may be a +terminal status, or it may still be `"Running"` if the phase limits or the +overall budget are exhausted before the workflow reaches a terminal state. + +##### Variants + +For a user-facing request, shorten the overall budget and the watchdog phase. +The user experience should usually return a clear "still running" response +rather than hold a request open for the full operational tail. + +For a back-office worker, lengthen the steady and watchdog phases but keep the +phase limits explicit. Long-running does not have to mean unbounded. + +For fleet-wide polling, keep jitter on the longer phases. The responsive phase +is short-lived, but the steady and watchdog phases contain the larger population +of long-lived pollers, so they are where synchronized checks create the most +load. + +For phase-specific telemetry, use `Schedule.andThenResult` instead of +`Schedule.andThen` on the boundary you need to observe. The result identifies +which side of the phase boundary produced the schedule output, which is useful +when metrics need separate labels for responsive, steady, and watchdog +behavior. + +##### Notes and caveats + +`Schedule.take(n)` limits the recurrences in that phase. It does not count the +initial status read before `Effect.repeat` starts using the schedule. + +`Schedule.during` measures elapsed time for the schedule it is combined with. +When it is added outside the phased cadence with `Schedule.both`, it acts as an +overall budget rather than a per-phase budget. + +`Schedule.spaced` waits after each status read completes. If the status read +itself can hang, put a timeout on `readWorkflowStatus`; the schedule controls +the delay between reads, not the duration of an individual read. + +Keep the terminal-state predicate near the schedule. The phase limits answer +"how long and how often should we observe?" The `Schedule.while` predicate +answers "is another observation still useful?" + +### 25. Express Operational Intent + +#### 25.1 “Try hard, but only briefly” + +Some failures deserve a real effort, but not a long wait. A request can hit a +short restart, a just-rotated connection, or a cache entry that is about to +appear. In those cases, the useful policy is not "retry forever" or "retry once" +but "try a few quick times inside a tiny window, then give the caller the +failure." + +Model that operational phrase as a composed schedule. One piece says how hard +to try, another says how brief the window is, and `Schedule.both` makes both +limits visible in the policy. + +##### Problem + +Turn that operational phrase into concrete limits for a retry schedule. The +policy should answer three questions directly: + +- how quickly to retry after a failure +- how many follow-up attempts are allowed +- how long the whole retry window may stay open + +The first attempt still runs immediately. `Schedule` controls only the +decisions after a typed failure. + +##### When to use it + +Use this recipe for cheap, idempotent operations where a short recovery window +is useful: reading from a local service, fetching small metadata, refreshing a +cache value, or calling an internal dependency during a deploy. + +It is a good fit when "try hard" means several quick attempts, not minutes of +persistence. For example, `Schedule.recurs(4)` means up to four retries after +the original attempt, so the effect can execute at most five times total. + +##### When not to use it + +Do not use this for permanent failures. Bad input, authorization failures, +missing resources, and rejected business rules should usually fail without a +retry policy. + +Do not use it for expensive or unsafe operations unless the unit being retried +is idempotent. A short schedule can still repeat a side effect several times. + +Also avoid this policy when the dependency is already overloaded. In that case, +"try hard" can make the outage worse; use a slower backoff policy with jitter +instead. + +##### Schedule shape + +Compose a short fast cadence with a retry-count limit and an elapsed-time +budget. `Schedule.exponential("50 millis")` starts with a small delay and +increases it on each recurrence. `Schedule.recurs(4)` bounds the number of +retries. `Schedule.during("500 millis")` bounds the retry window. + +`Schedule.both` gives intersection semantics: the combined schedule recurs only +while both sides still want to recur, and it uses the larger delay from the +pieces being combined. The result is a policy that tries quickly, stops by +count, and also stops when the short time budget is exhausted. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type DependencyError = { + readonly _tag: "DependencyUnavailable" + readonly service: string +} + +let attempts = 0 + +const readFromDependency = Effect.gen(function*() { + attempts++ + yield* Console.log(`read attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail( + { + _tag: "DependencyUnavailable", + service: "catalog" + } satisfies DependencyError + ) + } + + return "catalog metadata" +}) + +const tryHardButBriefly = Schedule.exponential("20 millis").pipe( + Schedule.both(Schedule.recurs(4)), + Schedule.both(Schedule.during("250 millis")) +) + +const program = readFromDependency.pipe( + Effect.retry(tryHardButBriefly), + Effect.flatMap((value) => Console.log(`result: ${value}`)) +) + +Effect.runPromise(program) +// Output: +// read attempt 1 +// read attempt 2 +// read attempt 3 +// read attempt 4 +// result: catalog metadata +``` + +`program` performs the first dependency read immediately. If it fails with +`DependencyUnavailable`, the retry policy starts with a small delay, then grows +from there, while the count limit and elapsed budget both remain open. If either +limit is exhausted, `Effect.retry` returns the last typed failure. + +##### Variants + +For an even tighter user-facing path, reduce the budget and retry count, for +example `Schedule.exponential("25 millis")` with `Schedule.recurs(2)` and a +`Schedule.during("150 millis")` budget. + +For a small background task where a brief recovery window is still acceptable, +increase the budget slightly but keep the policy visibly bounded. + +If many clients or workers can hit the same dependency at once, add +`Schedule.jittered` after the basic cadence and limits are correct. + +##### Notes and caveats + +`Schedule.during` is checked at recurrence decision points. It does not +interrupt an in-flight dependency call. If one attempt also needs a hard +deadline, add a timeout to the effect being retried. + +`Schedule.recurs` counts retries after the original attempt. With +`Schedule.recurs(4)`, the effect can run up to five times total. + +`Effect.retry` feeds failures into the schedule. Classify permanent failures +before applying this policy, or use a schedule predicate when only some typed +errors are retryable. + +#### 25.2 “Keep trying, but never aggressively” + +Some work should keep trying for as long as the process is alive, but it should +never turn a failure into pressure on an already weak dependency. Use a slow +`Schedule.spaced` cadence when persistence matters more than fast recovery. + +Read the policy as: after each retryable failure, wait for a deliberate pause +before trying again. `Schedule.jittered` keeps the delay near that cadence while +preventing a fleet of workers from retrying at exactly the same instant. + +##### Problem + +Apply this shape to background work such as refreshing a cache, reconnecting to +a secondary service, resending an idempotent notification, or checking whether a +dependency has come back. + +The policy should make three facts visible: + +- retryable failures may be retried indefinitely +- every retry leaves a deliberate pause +- non-retryable failures still stop immediately + +##### When to use it + +Use this recipe for non-interactive workflows where eventual recovery is useful +and latency is not the primary concern. It is a good fit for background workers, +maintenance loops, cache warmers, telemetry delivery, and other idempotent work +that should continue quietly after transient outages. + +Use it when the operational requirement sounds like "keep trying in the +background" or "do not page someone just because the dependency was unavailable +for a while." + +##### When not to use it + +Do not use this for user-facing requests that need a timely answer. A persistent +retry policy can leave the caller waiting forever unless the surrounding effect +has its own timeout or cancellation boundary. + +Do not use it to retry permanent failures. Invalid configuration, malformed +input, missing authorization, and unsafe non-idempotent writes should be +classified before this schedule is applied. + +Do not use a short spacing just because the schedule is simple. If the work is +allowed to continue forever, the delay should be generous enough to be safe +during an extended outage. + +##### Schedule shape + +`Schedule.spaced("30 seconds")` recurs indefinitely and waits 30 seconds between +recurrence decisions. With `Effect.retry`, the first execution of the effect is +still immediate; the schedule controls only the retries after failures. + +`Schedule.jittered` adjusts each computed delay to a random value between 80% +and 120% of the original delay. For a 30 second base cadence, retries happen +roughly between 24 and 36 seconds apart. That keeps the policy low pressure +while avoiding synchronized retries across many workers. + +There is intentionally no `Schedule.recurs` or `Schedule.during` in the base +policy. Persistence is the point of this recipe. The stopping condition belongs +to error classification, shutdown, cancellation, or a separate business rule. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class DeliveryError extends Data.TaggedError("DeliveryError")<{ + readonly reason: "Network" | "Unavailable" | "BadRecipient" | "InvalidPayload" +}> {} + +let attempts = 0 + +const deliverNotification = Effect.gen(function*() { + attempts++ + yield* Console.log(`delivery attempt ${attempts}`) + + if (attempts < 4) { + return yield* Effect.fail( + new DeliveryError({ reason: "Unavailable" }) + ) + } + + yield* Console.log("notification delivered") +}) + +const isRecoverable = (error: DeliveryError) => error.reason === "Network" || error.reason === "Unavailable" + +const lowPressureRetry = Schedule.spaced("40 millis").pipe( + Schedule.jittered +) + +const program = deliverNotification.pipe( + Effect.retry({ + schedule: lowPressureRetry, + while: isRecoverable + }) +) + +Effect.runPromise(program) +// Output: +// delivery attempt 1 +// delivery attempt 2 +// delivery attempt 3 +// delivery attempt 4 +// notification delivered +``` + +The demo uses a short delay so it terminates quickly. In production, choose a +low-pressure interval such as 30 seconds or several minutes. The first delivery +attempt runs immediately. If it fails with `Network` or `Unavailable`, the +program waits for the spaced cadence and tries again. + +If the delivery succeeds, `program` succeeds. If the error is `BadRecipient` or +`InvalidPayload`, the retry predicate returns `false` and `program` fails with +that error instead of spending more time on a permanent problem. + +##### Variants + +Use a longer spacing when the dependency is shared or expensive. A five-minute +cadence can be appropriate for secondary background recovery. + +Add an elapsed budget only when persistence is no longer the requirement. A +policy that combines `Schedule.spaced("30 seconds")`, `Schedule.jittered`, and +`Schedule.during("1 hour")` still retries gently, but it stops once the elapsed +window is closed. + +Use an exponential policy when fast early recovery matters. That is a different +operational promise: it tries sooner at first, then backs off, and eventually +gives up. + +##### Notes and caveats + +`Effect.retry` feeds failures into the schedule. The `while` predicate is where +this recipe separates recoverable operational failures from permanent domain +failures. + +`Schedule.spaced` waits after a failed attempt completes. It does not place a +timeout on an attempt that is already running. Add a timeout to +`deliverNotification` itself if each attempt needs a maximum duration. + +Because this policy can retry forever, observability matters. Log or metric the +failure near the effect being retried, but keep the schedule focused on the +recurrence policy: low-pressure spacing, jitter, and no artificial retry count. + +#### 25.3 “Be responsive first, conservative later” + +Some failures are worth a fast second look, but not an indefinitely fast one. A +cache refresh, leader-election read, or request to a nearby dependency might +clear on the next attempt. If it does not, the policy should slow down before it +adds pressure to the same system it is waiting on. + +Encode that intent as phases: a responsive phase first, then a conservative +phase. The schedule value tells the reader when the workflow switches from "try +again soon" to "back off and give the dependency room." + +##### Problem + +A single exponential schedule can express growing delay, but it does not name +the operational transition. Put that transition in the schedule value so the +responsive and conservative phases can be tuned independently. + +##### When to use it + +Use this when the first few failures are likely to be local, brief, or caused by +startup ordering, but continued failure should be treated as pressure on a +shared dependency. It is a good fit for retries around idempotent reads, +connection establishment, cache warming, discovery calls, and background +reconciliation loops. + +##### When not to use it + +Do not use this to blur permanent failures into retryable failures. Validation +errors, authorization failures, malformed requests, and unsafe non-idempotent +writes should be classified before the schedule is applied. + +Avoid this shape when the caller needs a strict latency budget. In that case, +compose the retry schedule with a time limit or move the work out of the request +path. + +##### Schedule shape + +Build two named schedules and sequence them with `Schedule.andThen`. + +- The first phase is short and responsive. It uses a small exponential delay and + a low `Schedule.take` count. +- The second phase is conservative. It starts at a larger delay and is also + bounded. + +`Schedule.andThen(first, second)` runs the first schedule to completion, then +continues with the second schedule. The first execution of the effect is not +part of the schedule; the schedule controls the follow-up attempts after each +failure. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type TransientError = { readonly _tag: "TransientError" } + +let attempts = 0 + +const refreshRemoteSnapshot = Effect.gen(function*() { + attempts++ + yield* Console.log(`refresh attempt ${attempts}`) + + if (attempts < 5) { + return yield* Effect.fail( + { + _tag: "TransientError" + } satisfies TransientError + ) + } + + return "snapshot refreshed" +}) + +const responsivePhase = Schedule.exponential("15 millis").pipe( + Schedule.take(3) +) + +const conservativePhase = Schedule.exponential("80 millis").pipe( + Schedule.take(4) +) + +const responsiveThenConservative = Schedule.andThen( + responsivePhase, + conservativePhase +) + +const program = refreshRemoteSnapshot.pipe( + Effect.retry(responsiveThenConservative), + Effect.flatMap((value) => Console.log(value)) +) + +Effect.runPromise(program) +// Output: +// refresh attempt 1 +// refresh attempt 2 +// refresh attempt 3 +// refresh attempt 4 +// refresh attempt 5 +// snapshot refreshed +``` + +The first few retry decisions come from the responsive phase. If the operation +keeps failing, the conservative phase takes over with larger delays and its own +limit. + +##### Variants + +For user-facing requests, keep both phases small or add an outer timeout around +the whole operation so the caller gets a predictable answer. + +For background workers, make the conservative phase longer and add logging or +metrics with `Schedule.tapInput` or `Schedule.tapOutput`. + +For fleet-wide retries, add jitter after the base cadence is correct so many +instances do not retry at the same boundaries. + +If operators need to distinguish the phase in telemetry, use +`Schedule.andThenResult` instead of `Schedule.andThen`; its output records +whether the current recurrence came from the first or second phase. + +##### Notes and caveats + +With `Effect.retry`, failures are fed into the schedule. That matters if you +observe inputs with `Schedule.tapInput` or stop based on the error value with +`Schedule.while`. Keep error classification close to the effect being retried, +then let the schedule describe only recurrence mechanics: responsive phase, +conservative phase, and final stop condition. + +#### 25.4 “Avoid overload at all costs” + +When the requirement is "avoid overload at all costs", the retry policy should +prefer giving up over adding pressure to a dependency that is already +struggling. That means conservative spacing, increasing waits, fleet-wide +desynchronization, and explicit limits. + +Use the schedule to make that operational promise reviewable. A reader should +be able to see the first retry delay, the backoff curve, the maximum final +delay, the retry count, and the elapsed budget without hunting through a custom +loop. + +##### Problem + +Define a retry schedule for callers seeing a slow, unavailable, or rate-limited +downstream service. The schedule must make overload control explicit: +conservative initial delay, growing waits, jitter, a maximum wait, and finite +count and time limits. + +##### When to use it + +Use this for shared infrastructure paths where extra traffic is more dangerous +than a delayed or failed caller response: broker reconnects, cache refreshes, +webhook delivery, background synchronization, dependency readiness checks, and +batch workers. + +It is especially useful when many processes may observe the same outage at the +same time. The policy should spread retries across the fleet and cap the amount +of work each caller contributes. + +##### When not to use it + +Do not use this policy for validation failures, authorization failures, +permanent configuration errors, or unsafe non-idempotent writes. Classify those +before retrying and fail without entering the schedule. + +Do not treat client-side backoff as admission control. It reduces retry +pressure, but it does not replace server-side rate limits, quotas, queues, +backpressure, load shedding, or circuit breaking. + +##### Schedule shape + +Start with a slow exponential backoff, add jitter, cap the final delay, and add +both count and elapsed-time limits. `Schedule.exponential("2 seconds")` starts +with a two-second delay and then grows by the default factor of `2`. That is +intentionally slower than a latency-oriented retry policy. + +`Schedule.jittered` adjusts each recurrence delay between 80% and 120% of the +incoming delay. If many workers fail at the same time, their later retries are +less likely to stay synchronized. + +Use `Schedule.modifyDelay` to cap the final delay after jitter. Add +`Schedule.recurs` and `Schedule.during` with `Schedule.both` so the policy stops +when either the retry count or elapsed budget is exhausted. + +##### Example + +```ts runnable deterministic +import { Console, Duration, Effect, Schedule } from "effect" + +type InventorySnapshot = { + readonly sku: string + readonly available: number +} + +type DownstreamError = + | { readonly _tag: "Timeout"; readonly service: string } + | { readonly _tag: "Unavailable"; readonly service: string } + | { readonly _tag: "RateLimited"; readonly service: string } + | { readonly _tag: "Rejected"; readonly service: string } + +const isRetryable = (error: DownstreamError): boolean => + error._tag === "Timeout" || + error._tag === "Unavailable" || + error._tag === "RateLimited" + +let attempts = 0 + +const loadInventorySnapshot = Effect.gen(function*() { + attempts++ + yield* Console.log(`inventory attempt ${attempts}`) + + if (attempts === 1) { + return yield* Effect.fail( + { + _tag: "RateLimited", + service: "inventory" + } satisfies DownstreamError + ) + } + if (attempts < 4) { + return yield* Effect.fail( + { + _tag: "Unavailable", + service: "inventory" + } satisfies DownstreamError + ) + } + + return { + sku: "sku-123", + available: 42 + } satisfies InventorySnapshot +}) + +const avoidOverloadRetryPolicy = Schedule.exponential("40 millis").pipe( + Schedule.jittered, + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.millis(120)))), + Schedule.both(Schedule.recurs(8)), + Schedule.both(Schedule.during("500 millis")) +) + +const program = loadInventorySnapshot.pipe( + Effect.retry({ + schedule: avoidOverloadRetryPolicy, + while: isRetryable + }), + Effect.flatMap((snapshot) => Console.log(`${snapshot.sku}: ${snapshot.available} available`)) +) + +Effect.runPromise(program) +// Output: +// inventory attempt 1 +// inventory attempt 2 +// inventory attempt 3 +// inventory attempt 4 +// sku-123: 42 available +``` + +The demo uses short durations so it finishes quickly. In production, the same +shape would usually start at seconds, cap at tens of seconds or minutes, and +use an operational budget that matches the caller. + +`program` performs the first call immediately. If it fails with a retryable +typed error, the retry schedule waits for a jittered exponential delay before +trying again. If the error is `"Rejected"`, the `while` predicate prevents the +retry policy from adding more traffic. + +The policy allows at most eight retries after the original attempt, and only +while the elapsed budget is still open. If every allowed retry fails, +`Effect.retry` propagates the last typed failure. + +##### Variants + +For interactive requests, make the policy stricter: start closer to one second, +cap at a few seconds, and use a small retry count or a short `Schedule.during` +budget. Avoid making a user wait through a background-worker retry profile. + +For background recovery jobs, use a larger base delay and a smaller retry count +when the dependency is known to be fragile. A policy such as "start at 10 +seconds, cap at 2 minutes, retry 5 times" is often clearer than trying to keep +a failing workflow alive indefinitely. + +For APIs that return a trusted retry-after signal, keep this overload policy as +the default and handle the server-provided delay in a separate, named policy. +Do not mix "server told us when to return" with "client chose a conservative +backoff" unless the composition is still obvious in review. + +##### Notes and caveats + +`Schedule.exponential`, `Schedule.spaced`, and `Schedule.jittered` do not stop +by themselves. Pair them with `Schedule.recurs`, `Schedule.take`, +`Schedule.during`, or an input-aware condition. + +`Schedule.jittered` in Effect uses an 80%-120% range. If the final maximum +delay must be strict, cap after jitter with `Schedule.modifyDelay`. + +`Effect.retry` feeds failures into the schedule. `Effect.repeat` feeds +successful values into the schedule. This recipe is about retrying typed +transient failures; polling successful observations should use `Effect.repeat` +and a success-value stop condition instead. + +#### 25.5 “Keep background work steady and predictable” + +Steady background work should be easy to explain: run the task, wait a known +amount of time, then run it again. The schedule should not look like a retry +policy, a catch-up mechanism, or a hidden control loop unless those behaviors +are actually required. + +For most maintenance work, `Schedule.spaced` is the clearest contract. It waits +for the interval after a successful run completes. That makes the load +predictable from the worker's point of view: one run at a time, followed by one +deliberate pause. + +##### Problem + +Represent a recurring maintenance task such as refreshing a cache, reconciling +records, publishing metrics, or pruning expired state with the simplest schedule +that states its cadence. Keep normal cadence separate from retry, catch-up, or +overload-control behavior. + +##### When to use it + +Use this recipe when the important property is a stable gap between completed +runs. It fits work where freshness is approximate, overlapping runs would be +undesirable, and the next run should naturally move later if the current run +takes longer than usual. + +This is also a good default when operators need a simple answer to "how often +does this worker create load?" With a spaced schedule, the answer is the run +duration plus the configured pause. + +##### When not to use it + +Do not use this as failure recovery by accident. `Effect.repeat` repeats +successful values; the first failure stops the repeated effect. If transient +failures should be retried, handle that inside the repeated operation or use a +separate retry policy around the part that can fail. + +Do not use `Schedule.spaced` when starts must stay close to wall-clock +boundaries. Use `Schedule.fixed` for that shape. A fixed schedule maintains an +interval-based cadence and, if a run takes longer than the interval, the next +run may happen immediately; missed runs do not pile up. + +Avoid adding jitter, elapsed budgets, and count limits just to make the policy +feel more robust. Add each piece only when it states a real operational +constraint. + +##### Schedule shape + +Start with a named cadence such as `Schedule.spaced("30 seconds")`. It outputs +the recurrence count and delays each follow-up run by thirty seconds after the +previous successful run completes. The initial run is not delayed by the +schedule; `Effect.repeat` runs the effect once before consulting the schedule. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type RefreshError = { readonly _tag: "RefreshError" } + +let refreshes = 0 + +const refreshSearchIndex: Effect.Effect = Effect.gen(function*() { + refreshes++ + yield* Console.log(`refresh ${refreshes}`) +}) + +const backgroundCadence = Schedule.spaced("40 millis") + +const demoCadence = backgroundCadence.pipe( + Schedule.take(3) +) + +const program = refreshSearchIndex.pipe( + Effect.repeat(demoCadence), + Effect.flatMap(() => Console.log("demo complete")) +) + +Effect.runPromise(program) +// Output: +// refresh 1 +// refresh 2 +// refresh 3 +// refresh 4 +// demo complete +``` + +The demo bounds the repeat with `Schedule.take(3)` so it terminates quickly. +For a supervised worker, the cadence would usually stay unbounded and the +supervisor or application scope would own shutdown. + +The policy says only one thing: after a successful refresh, wait before +refreshing again. If `refreshSearchIndex` fails, `program` fails. That is +usually better than silently continuing with an unclear health state. + +##### Variants + +Use `Schedule.fixed("30 seconds")` when the cadence itself is the contract, +such as a metrics flush that should stay close to a regular interval. Slow runs +can cause the next recurrence to happen immediately, but fixed scheduling does +not enqueue every missed interval. + +Use `Schedule.spaced("30 seconds").pipe(Schedule.take(10))` for a bounded +diagnostic or migration pass. The first run still happens immediately, followed +by up to ten scheduled recurrences. + +Use `Schedule.jittered` only when many instances running the same cadence would +otherwise synchronize and create fleet-wide spikes. Jitter is useful for +aggregate load, but it makes a single worker less predictable. + +##### Notes and caveats + +`Schedule.spaced` measures the delay after completion. `Schedule.fixed` aims at +fixed interval boundaries. Choose between them before adding any other +combinator. + +Keep failure handling separate from cadence. A common shape is a small retry +policy inside one background iteration, followed by a simple repeat schedule +around the iteration. That keeps "recover this attempt" distinct from "run the +background task again later." + +An unbounded repeat is long-lived work. Give it an owner such as a scope, +supervisor, or shutdown race so it can be interrupted deliberately. + +## Part VII — Real-World Recipes + +### 26. Backend Recipes + +#### 26.1 Retry HTTP GET on timeout + +Retry a `GET` only when the endpoint is safe to repeat and the failure is a +temporary transport problem. + +##### Problem + +You call `GET /users/:id`. The request can time out, return a non-success HTTP +status, or produce a response that cannot be decoded. + +In this recipe, only the timeout is retryable. Authentication failures, +authorization failures, missing resources, and decoding failures should return +immediately. Retrying them adds load without making the request valid. + +##### When to use it + +Use this for idempotent reads, where repeating the request has the same logical +effect as sending it once. It fits metadata lookups, status reads, configuration +fetches, and similar paths where a short delay is acceptable. + +Make the retryable condition explicit in the error model. A `HttpTimeout` tag is +clearer and safer than parsing exception messages. + +##### When not to use it + +Do not retry a `GET` blindly if it starts work, marks records as viewed, +advances a cursor, or depends on one-time credentials. HTTP method names are a +signal; the endpoint behavior is what matters. + +Do not leave the schedule unbounded. `Schedule.exponential("100 millis")` keeps +recurring unless you add a retry count, elapsed budget, or both. + +##### Schedule shape + +Use `Effect.retry` with a typed `while` predicate and a finite schedule. +`Schedule.exponential` spaces retries, `Schedule.jittered` avoids synchronized +clients, `Schedule.recurs(3)` allows three retries after the first request, and +`Schedule.during` adds an elapsed retry budget. `Schedule.both` means both +limits must still allow recurrence. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +interface User { + readonly id: string + readonly name: string +} + +class HttpTimeout extends Data.TaggedError("HttpTimeout")<{ + readonly url: string +}> {} + +class HttpStatusError extends Data.TaggedError("HttpStatusError")<{ + readonly url: string + readonly status: number +}> {} + +class DecodeError extends Data.TaggedError("DecodeError")<{ + readonly message: string +}> {} + +type GetUserError = HttpTimeout | HttpStatusError | DecodeError + +let attempts = 0 + +const httpGetJson = (url: string): Effect.Effect => + Effect.gen(function*() { + attempts += 1 + yield* Console.log(`GET ${url}, attempt ${attempts}`) + + if (attempts <= 2) { + return yield* Effect.fail(new HttpTimeout({ url })) + } + + return { id: "user-123", name: "Ada" } + }) + +const decodeUser = (body: unknown): Effect.Effect => { + if ( + typeof body === "object" && + body !== null && + "id" in body && + "name" in body && + typeof body.id === "string" && + typeof body.name === "string" + ) { + return Effect.succeed({ id: body.id, name: body.name }) + } + return Effect.fail(new DecodeError({ message: "Expected a user object" })) +} + +const getUser = Effect.fnUntraced(function*(id: string) { + const url = `/users/${id}` + const body = yield* httpGetJson(url) + return yield* decodeUser(body) +}) + +const isHttpTimeout = (error: GetUserError): error is HttpTimeout => error._tag === "HttpTimeout" + +const retryGetTimeouts = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(3)), + Schedule.both(Schedule.during("200 millis")) +) + +const program = getUser("user-123").pipe( + Effect.retry({ + schedule: retryGetTimeouts, + while: isHttpTimeout + }), + Effect.tap((user) => Console.log(`loaded ${user.name}`)) +) + +Effect.runPromise(program).then(console.log, console.error) +// Output: +// GET /users/user-123, attempt 1 +// GET /users/user-123, attempt 2 +// GET /users/user-123, attempt 3 +// loaded Ada +// { id: 'user-123', name: 'Ada' } +``` + +The example uses small delays so it terminates quickly. The first request is +immediate; only accepted timeout failures are delayed and retried. + +If the request fails with `HttpStatusError`, or if decoding fails with +`DecodeError`, the predicate returns `false` and the failure is returned without +another HTTP request. + +##### Variants + +For an interactive path, use fewer retries or a smaller elapsed budget. For a +background read path, keep the same timeout predicate but allow a wider bounded +policy. + +Keep the predicate separate from the timing policy. The predicate answers +whether the failure is retryable; the schedule answers how retrying proceeds +after that failure is accepted. + +##### Notes and caveats + +`Effect.retry` feeds typed failures from the effect's error channel into the +retry policy. The first HTTP request is not delayed. + +Timeouts are ambiguous: the server may have produced a response the client did +not receive. Retrying a `GET` is normally reasonable, but only when the specific +endpoint is actually idempotent. + +Bounded retry is part of the contract. A retry count protects the downstream +service from unbounded pressure; an elapsed budget protects the caller from +spending too long on one dependency. + +#### 26.2 Retry HTTP GET on 503 + +A `503 Service Unavailable` response can be retryable for an idempotent HTTP +`GET`. Keep it narrower than a generic HTTP retry policy. + +##### Problem + +You fetch a resource with `GET`. If the service responds with `503`, retry +briefly with backoff. If it responds with any other failure, return that failure +to the caller immediately. + +The predicate decides whether the current typed failure is retryable. The +schedule decides when another attempt is allowed and when retrying stops. + +##### When to use it + +Use this for idempotent HTTP reads where a `503` really means temporary +unavailability: dependency warm-up, rolling deploys, overloaded gateways, or a +backend pool with short-lived capacity trouble. + +It is a good fit when callers need an answer quickly but a small number of +retries can hide brief service interruptions. + +##### When not to use it + +Do not retry every HTTP status. A `400 Bad Request`, `401 Unauthorized`, +`403 Forbidden`, `404 Not Found`, or `422 Unprocessable Entity` usually needs a +different request, credentials, or domain decision. + +Do not treat `503` as the same thing as `429 Too Many Requests`. A rate-limit +response often needs `Retry-After` handling, client-side admission control, or a +different budget. + +Do not apply this recipe blindly to writes. `GET` is normally safe to retry; a +non-idempotent `POST` needs an idempotency key or another duplicate-safety +guarantee before adding retries. + +##### Schedule shape + +With `Effect.retry`, failures from the error channel are the schedule inputs. +Use the options form when retryability is a predicate over the typed error. + +Start with `Schedule.exponential`, then intersect it with explicit limits using +`Schedule.both`. The combined schedule recurs only while both sides still allow +another recurrence. For example, a backoff schedule combined with +`Schedule.recurs(4)` and `Schedule.during("3 seconds")` allows at most four +retries after the first request, and only while the elapsed retry budget is +still open. + +Add `Schedule.jittered` when many instances may hit the same service at once. +It adjusts each recurrence delay between 80% and 120% of the original delay, +which helps avoid synchronized retry waves. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +type HttpStatus = 200 | 400 | 401 | 403 | 404 | 429 | 500 | 502 | 503 | 504 + +interface HttpResponse { + readonly status: HttpStatus + readonly body: string +} + +class TransportError extends Data.TaggedError("TransportError")<{ + readonly url: string + readonly reason: string +}> {} + +class HttpResponseError extends Data.TaggedError("HttpResponseError")<{ + readonly method: "GET" + readonly url: string + readonly status: Exclude +}> {} + +type GetCatalogError = TransportError | HttpResponseError + +let attempts = 0 + +const rawGet = (url: string): Effect.Effect => + Effect.gen(function*() { + attempts += 1 + yield* Console.log(`GET ${url}, attempt ${attempts}`) + + if (attempts <= 2) { + return { status: 503, body: "warming up" } + } + + return { status: 200, body: "catalog-v1" } + }) + +const classifyGetResponse = ( + url: string, + response: HttpResponse +): Effect.Effect => + response.status === 200 + ? Effect.succeed(response.body) + : Effect.fail( + new HttpResponseError({ + method: "GET", + url, + status: response.status as Exclude + }) + ) + +const getCatalog = (url: string): Effect.Effect => + rawGet(url).pipe( + Effect.flatMap((response) => classifyGetResponse(url, response)) + ) + +const isServiceUnavailableGet = (error: GetCatalogError): boolean => + error._tag === "HttpResponseError" && + error.method === "GET" && + error.status === 503 + +const retry503WithBackoff = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.recurs(4)), + Schedule.both(Schedule.during("300 millis")), + Schedule.jittered +) + +const program = getCatalog("https://api.example.test/catalog").pipe( + Effect.retry({ + schedule: retry503WithBackoff, + while: isServiceUnavailableGet + }), + Effect.tap((body) => Console.log(`received ${body}`)) +) + +Effect.runPromise(program).then(console.log, console.error) +// Output: +// GET https://api.example.test/catalog, attempt 1 +// GET https://api.example.test/catalog, attempt 2 +// GET https://api.example.test/catalog, attempt 3 +// received catalog-v1 +// catalog-v1 +``` + +The example uses short delays so it finishes quickly. The first `GET` is sent +immediately. A `503` is retried; any other status is surfaced immediately. + +If the transport layer fails with `TransportError`, this policy also does not +retry it. That can be a separate timeout or network-failure recipe. + +If every permitted retry still receives `503`, retrying stops when the count +limit or elapsed-time limit is exhausted, and the last typed failure is +propagated. + +##### Variants + +Use a count-only policy when elapsed time is less important than a fixed number +of attempts. Use a shorter user-facing budget when the caller is waiting. + +If you want the reusable schedule itself to carry the 503 filter, use +`Schedule.while(({ input }) => isServiceUnavailableGet(input))` after setting +the schedule input type to the HTTP error. + +##### Notes and caveats + +The retry predicate is evaluated after a failed attempt. It cannot prevent the +initial request; it only decides whether another request should be attempted. + +`Schedule.recurs(4)` means four retries after the original attempt, not four +total HTTP requests. With the first request included, this policy can perform up +to five `GET` requests. + +`Schedule.during("3 seconds")` is checked at schedule decision points. It keeps +the retry window bounded, but it is not a timeout for the individual HTTP +request. Use request-level timeouts separately when a single attempt can hang. + +Keep non-503 classification close to the HTTP adapter. The schedule should not +parse response bodies or error messages to discover whether a failure is +retryable; it should receive a typed error that already carries the status code. + +#### 26.3 Retry HTTP POST with idempotency key + +HTTP `POST` retries need a duplicate-safety contract before any schedule is +added. An idempotency key is a request identifier the server uses to treat +repeated attempts as one logical write. + +##### Problem + +You need to retry an HTTP `POST` when the failure is ambiguous, such as a +timeout, dropped connection, gateway error, or temporary service outage. In +those cases, the client may not know whether the server committed the write. + +The retry must reuse the same idempotency key for every attempt. Generating a +fresh key inside the retried effect usually turns retries into independent +writes. + +##### When to use it + +Use this recipe when the downstream HTTP API explicitly supports idempotency +keys for the `POST` endpoint you are calling. Typical examples include payment +creation, order submission, subscription changes, shipment creation, and +command-style API calls. + +It is especially useful for failures where the outcome is unknown to the +client: a timeout after the request was sent, a connection reset before the +response arrived, a transient gateway error, or a retryable overload response. + +Create or load the idempotency key before entering `Effect.retry`, then pass +that same key to the HTTP request effect on every attempt. + +##### When not to use it + +Do not retry a non-idempotent `POST` unless the downstream service provides a +deduplication contract you can rely on. A local retry schedule cannot make an +unsafe write safe by itself. + +Do not retry permanent failures such as invalid payloads, authentication +failures, authorization failures, or domain rejections. Classify those errors +before retrying. + +Do not let the retry run forever. Idempotency keys reduce duplicate-write risk; +they do not remove load from a struggling downstream service. + +##### Schedule shape + +Use a bounded schedule for HTTP `POST` retries: + +Use exponential backoff so repeated failures slow down, jitter so many clients +do not retry at the same moment, a finite retry count, and an error predicate +that accepts only ambiguous or transient failures. + +`Schedule.recurs(4)` means four retries after the initial request. It is not a +total-attempt count. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class PostOrderError extends Data.TaggedError("PostOrderError")<{ + readonly reason: + | "Timeout" + | "ConnectionReset" + | "BadGateway" + | "ServiceUnavailable" + | "InvalidRequest" + | "Unauthorized" +}> {} + +interface Order { + readonly id: string + readonly status: "Created" | "AlreadyCreated" +} + +interface OrderRequest { + readonly customerId: string + readonly sku: string + readonly quantity: number + readonly idempotencyKey: string +} + +let attempts = 0 + +const postOrder = (request: OrderRequest): Effect.Effect => + Effect.gen(function*() { + attempts += 1 + yield* Console.log( + `POST /orders attempt ${attempts} with key ${request.idempotencyKey}` + ) + + if (attempts === 1) { + return yield* Effect.fail(new PostOrderError({ reason: "Timeout" })) + } + + return { + id: "order-1000", + status: attempts === 2 ? "Created" : "AlreadyCreated" + } + }) + +const retryPost = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const isRetryablePostFailure = (error: PostOrderError): boolean => { + switch (error.reason) { + case "Timeout": + case "ConnectionReset": + case "BadGateway": + case "ServiceUnavailable": + return true + case "InvalidRequest": + case "Unauthorized": + return false + } +} + +const submitOrder = Effect.fnUntraced(function*( + customerId: string, + sku: string, + quantity: number, + idempotencyKey: string +) { + return yield* postOrder({ customerId, sku, quantity, idempotencyKey }).pipe( + Effect.retry({ + schedule: retryPost, + while: isRetryablePostFailure + }) + ) +}) + +const program = submitOrder("customer-1", "sku-1", 2, "order-key-123").pipe( + Effect.tap((order) => Console.log(`order ${order.id}: ${order.status}`)) +) + +Effect.runPromise(program).then(console.log, console.error) +// Output: +// POST /orders attempt 1 with key order-key-123 +// POST /orders attempt 2 with key order-key-123 +// order order-1000: Created +// { id: 'order-1000', status: 'Created' } +``` + +The key detail is that `idempotencyKey` is an input to `submitOrder`. Every +attempt sends the same logical request with the same key. + +If the first `POST` succeeds on the server but the response is lost, a later +attempt with the same key should return the same logical result, such as +`Created` or `AlreadyCreated`, according to the downstream API contract. +`Schedule` only decides how many times to ask again and how much delay to put +between attempts. + +##### Variants + +For a user-facing request path, reduce the retry count so the caller gets a +prompt result. + +For a background worker or outbox processor, use a larger bounded policy and +persist the idempotency key with the job record. + +If the provider returns a specific "already processed" response for a repeated +key, model it as a successful domain value when it represents the same logical +write. Do not turn a successful deduplication response into another retryable +failure. + +##### Notes and caveats + +`Effect.retry` feeds failures into the schedule. The first `POST` attempt runs +immediately; the schedule controls only the follow-up attempts after failures. + +`Schedule.recurs(4)` means four retries after the original attempt, not four +total attempts. + +The idempotency key must identify one logical command. Reusing the same key for +a different payload can cause the downstream service to reject the request or +return a previous result for the wrong local intent. + +Check the downstream API's idempotency-key retention window. Some services +deduplicate keys for hours or days, not forever. Your retry and reconciliation +workflow should fit within that documented window. + +#### 26.4 Retry rate-limited requests carefully + +Rate-limit retries need a different shape from generic transient-error retries: +they should reduce pressure and honor server guidance such as `Retry-After`. + +##### Problem + +A downstream HTTP API sometimes responds with `429` and may include a +`Retry-After` value. You want to retry those responses without turning every +HTTP failure into a retry and without ignoring a server-supplied delay that is +longer than your local backoff. + +The first request still happens outside the schedule. The schedule controls only +the follow-up attempts after a failed request is classified as retryable. + +##### When to use it + +Use this recipe for idempotent requests, safe reads, or writes protected by an +idempotency key when the remote service explicitly reports rate limiting. It is +also useful for background workers that call APIs with shared tenant, account, +or application quotas. + +The important precondition is classification. Convert raw HTTP failures into a +small domain error first, and retry only the `RateLimited` case. Timeouts and +`503` responses may have their own retry policy, but `400`, `401`, `403`, `404`, +validation failures, and unsafe non-idempotent writes should not be hidden +behind a rate-limit schedule. + +##### When not to use it + +Do not use this as a generic HTTP retry wrapper. A rate limit says "wait before +asking again"; it does not say the original request is valid, safe to replay, or +worth retrying forever. + +Also avoid short fixed delays such as "retry every 100 millis" for `429` +responses. They make recovery look fast in tests but create exactly the kind of +pressure that the server is trying to reduce. + +##### Schedule shape + +Build the policy from four parts: + +`Schedule.exponential` spaces retries progressively. `Schedule.jittered` +spreads clients so they do not retry in lockstep. `Schedule.recurs` caps the +number of follow-up attempts. `Schedule.while` stops immediately when the +failure is not rate limited. + +To honor `Retry-After`, combine the backoff schedule with `Schedule.identity`. +`Effect.retry` feeds each failure into the schedule, so `identity` lets the +schedule output the current error. Then `Schedule.modifyDelay` can choose the +larger of the local backoff delay and the server-provided retry delay. + +##### Example + +```ts runnable deterministic +import { Console, Duration, Effect, Schedule } from "effect" + +type HttpError = + | { + readonly _tag: "RateLimited" + readonly retryAfter: Duration.Duration | undefined + } + | { + readonly _tag: "Unauthorized" | "Forbidden" | "BadRequest" | "Unavailable" + } + +let attempts = 0 + +const callApi: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`calling API, attempt ${attempts}`) + + if (attempts === 1) { + return yield* Effect.fail( + { + _tag: "RateLimited", + retryAfter: Duration.millis(30) + } as const + ) + } + + return "accepted" +}) + +const rateLimitPolicy = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.identity()), + Schedule.modifyDelay(([_, error], delay) => + Effect.succeed( + error._tag === "RateLimited" && error.retryAfter !== undefined + ? Duration.max(delay, error.retryAfter) + : delay + ) + ), + Schedule.both(Schedule.recurs(5)), + Schedule.while(({ input }) => input._tag === "RateLimited") +) + +const program = Effect.retry(callApi, rateLimitPolicy).pipe( + Effect.tap((result) => Console.log(`result: ${result}`)) +) + +Effect.runPromise(program).then(console.log, console.error) +// Output: +// calling API, attempt 1 +// calling API, attempt 2 +// result: accepted +// accepted +``` + +##### Variants + +If the provider returns `Retry-After` as a header, parse it before constructing +the domain error. Header parsing belongs with HTTP decoding, not inside the +schedule. Store the parsed value as a `Duration.Duration`, reject invalid or +negative values, and consider clamping very large values to the caller's +business deadline. + +For user-facing calls, keep the recurrence count small and combine the policy +with a short elapsed-time budget so the user gets a clear answer. For background +workers, use a larger base delay and let the queue or work scheduler re-enqueue +the job when the server asks for a long pause. + +For APIs with per-tenant quotas, include tenant or account information in +metrics around the retried effect. The schedule controls local timing; it does +not coordinate all callers that share the same quota. + +##### Notes and caveats + +`Schedule.both` continues only while both schedules continue, uses the maximum +delay from the two sides, and returns both outputs as a tuple. In this recipe the +timing side provides the backoff delay, while `Schedule.identity` carries the +current `HttpError` into `modifyDelay`. The recipe then chooses the larger of +the local backoff delay and the parsed `Retry-After` delay. + +`Schedule.while` sees schedule metadata, including the latest retry input. When +the predicate returns `false`, the retry stops and the original failure remains +visible to the caller. That is what you want for carefully classified HTTP +errors: retry `429`, but surface authorization, validation, and other permanent +failures immediately. + +#### 26.5 Poll a job-based HTTP API + +Job-based HTTP APIs are polling problems when the status endpoint returns +successful "still running" responses rather than errors. + +##### Problem + +You submit a job to an HTTP API and receive a `jobId`. The status endpoint can +return `"queued"` or `"running"` for a while before returning a terminal +`"succeeded"` or `"failed"` status. + +You need the first status check to happen right away, but you do not want an +unbounded loop. Readers should be able to see the polling interval, the terminal +condition, and the deadline in one place. + +##### When to use it + +Use this recipe when an API models long-running work as a job resource and the +status response is a successful domain value. A pending status is not an error; +it is the input that tells the schedule whether another poll should happen. + +This is a good fit for export generation, report rendering, media processing, +provisioning, and other backend workflows where completion is eventually visible +through a status endpoint. + +##### When not to use it + +Do not use polling to hide request errors. Authorization failures, invalid job +IDs, decode failures, and transient transport failures should stay in the error +channel and be handled separately from domain statuses. + +Also prefer a webhook, queue message, server-sent event, or direct callback when +the system already offers a push-based completion signal. + +##### Schedule shape + +Combine a spaced cadence, a terminal-state predicate, and an elapsed budget. +`Schedule.spaced("2 seconds")` waits after each successful status response +before the next poll. `Schedule.while` allows another recurrence only while the +latest status is non-terminal. `Schedule.during("1 minute")` gives the polling +loop an elapsed budget. + +`Schedule.passthrough` makes the latest `JobStatus` the schedule output. +`Schedule.bothLeft` adds the deadline while preserving that status output. + +##### Example + +```ts runnable +import { Console, Effect, Schedule } from "effect" + +type JobStatus = + | { readonly state: "queued"; readonly jobId: string } + | { readonly state: "running"; readonly jobId: string; readonly progress: number } + | { readonly state: "succeeded"; readonly jobId: string; readonly artifactUrl: string } + | { readonly state: "failed"; readonly jobId: string; readonly reason: string } + +type JobStatusError = { + readonly _tag: "JobStatusError" + readonly message: string +} + +const isTerminal = (status: JobStatus): boolean => status.state === "succeeded" || status.state === "failed" + +let polls = 0 + +const readJobStatus = (jobId: string): Effect.Effect => + Effect.gen(function*() { + polls += 1 + + const status: JobStatus = polls === 1 + ? { state: "queued", jobId } + : polls === 2 + ? { state: "running", jobId, progress: 60 } + : { state: "succeeded", jobId, artifactUrl: "/exports/job-1.csv" } + + yield* Console.log(`poll ${polls}: ${status.state}`) + return status + }) + +const pollJobUntilTerminalOrDeadline = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => !isTerminal(input)), + Schedule.bothLeft( + Schedule.during("200 millis").pipe( + Schedule.satisfiesInputType() + ) + ) +) + +const waitForJob = (jobId: string) => + readJobStatus(jobId).pipe( + Effect.repeat(pollJobUntilTerminalOrDeadline), + Effect.tap((status) => Console.log(`final status: ${status.state}`)) + ) + +Effect.runPromise(waitForJob("job-1")).then(console.log, console.error) +// Output may vary: elapsed timing can cross the polling budget boundary differently under load +// poll 1: queued +// poll 2: running +// poll 3: succeeded +// final status: succeeded +// { +// state: 'succeeded', +// jobId: 'job-1', +// artifactUrl: '/exports/job-1.csv' +// } +``` + +`waitForJob` performs the first status request immediately. If that first +response is `"succeeded"` or `"failed"`, it returns without waiting. If the +response is `"queued"` or `"running"`, the schedule waits two seconds before +polling again. + +The returned effect succeeds with the final observed `JobStatus`. That value is +terminal when the API returned `"succeeded"` or `"failed"` before the deadline. +It can still be `"queued"` or `"running"` when the one-minute polling budget was +used up first. + +##### Variants + +Use shorter spacing when the caller is waiting interactively and the status +endpoint is cheap. Use longer spacing for background jobs where completion +latency matters less than endpoint load. + +Add `Schedule.jittered` after the base cadence when many workers may begin +polling similar jobs at the same time. + +If each individual HTTP request needs its own deadline, put a timeout on +`readJobStatus(jobId)` separately. `Schedule.during` limits recurrence +decisions; it is not a hard timeout for an in-flight request. + +##### Notes and caveats + +`Effect.repeat` feeds successful values into the schedule. That is why the +predicate sees `JobStatus` values and can stop on terminal domain states. +`Effect.retry` would feed failures into the schedule instead. + +The first status request is not delayed. Schedules describe the recurrence after +the original effect has run. + +`Schedule.spaced` waits after a status request completes. Use `Schedule.fixed` +only when you need polling aligned to wall-clock intervals. + +Keep terminal domain states distinct from infrastructure failures. A job-level +`"failed"` status is a successful HTTP observation that should stop polling; a +failed status request is an effect failure that should be retried, reported, or +classified outside this repeat schedule. + +### 27. Frontend and Client Recipes + +#### 27.1 Retry config fetch at startup + +Startup configuration fetches sit on the first-render path, where a tiny outage +should not leave the UI stuck on a loading screen. + +##### Problem + +You want the first config request to happen immediately, retry a few transient +failures with increasing delay, and then stop so the client can show a clear +degraded state. + +##### When to use it + +Use this for read-only startup fetches where a retry can realistically recover: +a timeout, a brief network drop, a `503`, or a short CDN edge failure. The +schedule should be small enough that the maximum wait before fallback is easy to +explain. + +##### When not to use it + +Do not retry configuration errors that are deterministic for this client: +malformed JSON, an unsupported app version, a missing tenant, or an +authorization failure. Those should fail fast and route the user to an upgrade, +sign-in, or support path. + +Avoid a long startup retry loop for optional configuration. Render with defaults +and refresh in the background instead. + +##### Schedule shape + +Use exponential spacing combined with a small retry count. `Effect.retry` runs +the fetch once before consulting the schedule; the schedule describes the +follow-up attempts after failures. `Schedule.recurs(3)` means at most three +retries after the initial request. + +Add `while` classification so deterministic configuration failures do not spend +the transient-failure budget. + +##### Example + +```ts runnable +import { Console, Effect, Schedule } from "effect" + +type ClientConfig = { + readonly apiBaseUrl: string + readonly featureFlags: ReadonlyArray +} + +type ConfigFetchError = + | { readonly _tag: "NetworkUnavailable" } + | { readonly _tag: "ServiceUnavailable" } + | { readonly _tag: "MalformedConfig" } + +let attempts = 0 + +const fetchStartupConfig: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`fetch config attempt ${attempts}`) + + if (attempts <= 2) { + return yield* Effect.fail({ _tag: "ServiceUnavailable" } as const) + } + + return { + apiBaseUrl: "https://api.example.test", + featureFlags: ["new-profile"] + } +}) + +const isTransientConfigFailure = (error: ConfigFetchError): boolean => + error._tag === "NetworkUnavailable" || error._tag === "ServiceUnavailable" + +const startupConfigRetryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +const loadStartupConfig = fetchStartupConfig.pipe( + Effect.retry({ + schedule: startupConfigRetryPolicy, + while: isTransientConfigFailure + }), + Effect.tap((config) => Console.log(`loaded config for ${config.apiBaseUrl}`)) +) + +Effect.runPromise(loadStartupConfig).then(console.log, console.error) +// Output may vary: elapsed timing can cross the monitoring budget boundary differently under load +// fetch config attempt 1 +// fetch config attempt 2 +// fetch config attempt 3 +// loaded config for https://api.example.test +// { +// apiBaseUrl: 'https://api.example.test', +// featureFlags: [ 'new-profile' ] +// } +``` + +##### Variants + +For a very latency-sensitive first paint, reduce the retry count or use a +shorter base delay and fall back to cached defaults. For a config request made +by many clients at once, add jitter after choosing the base cadence so clients +do not retry in lockstep. For mandatory configuration, keep the retry policy +bounded but show a blocking error with a manual retry button after exhaustion. + +##### Notes and caveats + +Bounded startup retries protect the user experience as much as the service. A +schedule that retries forever can make the app look broken, while a schedule +that gives up too quickly can turn a tiny outage into a visible failure. + +Keep permanent error classification near `fetchStartupConfig`, keep the retry +policy short, and make the post-retry UI behavior explicit. + +#### 27.2 Retry profile loading on transient network failure + +Profile reads are user-facing, but they are usually safe to retry only when the +failure is clearly transient. + +##### Problem + +You need to load the signed-in user's profile in a frontend flow. The initial +request may fail for reasons that are worth retrying, such as the browser being +temporarily offline or the server returning `502`, `503`, or `504`. Other +failures are terminal for this interaction and should reach the UI immediately. + +The retry policy should retry only classified transient failures and stop after +a small number of attempts so the screen can show an actionable failure state. + +##### When to use it + +Use this recipe for idempotent profile reads where a short delay is acceptable +and a successful retry improves the user experience. It fits page boot, account +menus, settings screens, and other client reads where the same GET can be safely +attempted again. + +##### When not to use it + +Do not use this policy for authentication failures, validation errors, `404` +responses, or other outcomes that another attempt cannot fix. Also avoid it for +profile writes: changing display names, avatars, or preferences needs separate +idempotency and conflict-handling rules. + +##### Schedule shape + +Use a short exponential backoff, add jitter so many clients do not retry at the +same instant, and combine it with `Schedule.recurs` to cap retries. Because +`Effect.retry` sees each typed failure, use the retry `while` predicate to +continue only while the failure is classified as transient. + +The combined schedule stops as soon as either condition stops recurring: the +error is no longer transient, or the retry count has been exhausted. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +interface Profile { + readonly id: string + readonly name: string +} + +interface HttpResponse { + readonly status: number + readonly body: unknown +} + +class ProfileLoadError extends Data.TaggedError("ProfileLoadError")<{ + readonly reason: + | "BadResponse" + | "Forbidden" + | "NotFound" + | "Offline" + | "ServerUnavailable" + readonly status?: number + readonly cause?: unknown +}> {} + +const isTransient = (error: ProfileLoadError): boolean => + error.reason === "Offline" || error.reason === "ServerUnavailable" + +const classifyHttpStatus = (response: HttpResponse): ProfileLoadError => { + if (response.status === 401 || response.status === 403) { + return new ProfileLoadError({ reason: "Forbidden", status: response.status }) + } + if (response.status === 404) { + return new ProfileLoadError({ reason: "NotFound", status: response.status }) + } + if (response.status === 502 || response.status === 503 || response.status === 504) { + return new ProfileLoadError({ reason: "ServerUnavailable", status: response.status }) + } + return new ProfileLoadError({ reason: "BadResponse", status: response.status }) +} + +const retryTransientProfileLoad = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(3)) +) + +const decodeProfile = (body: unknown): Effect.Effect => { + if ( + typeof body === "object" && + body !== null && + "id" in body && + "name" in body && + typeof body.id === "string" && + typeof body.name === "string" + ) { + return Effect.succeed({ id: body.id, name: body.name }) + } + return Effect.fail(new ProfileLoadError({ reason: "BadResponse", cause: body })) +} + +let attempts = 0 + +const requestProfile = (userId: string): Effect.Effect => + Effect.gen(function*() { + attempts += 1 + yield* Console.log(`load profile ${userId}, attempt ${attempts}`) + + if (attempts === 1) { + return yield* Effect.fail(new ProfileLoadError({ reason: "Offline" })) + } + if (attempts === 2) { + return { status: 503, body: "service unavailable" } + } + return { status: 200, body: { id: userId, name: "Ada" } } + }) + +const fetchProfile = (userId: string): Effect.Effect => + requestProfile(userId).pipe( + Effect.flatMap((response) => + response.status === 200 + ? decodeProfile(response.body) + : Effect.fail(classifyHttpStatus(response)) + ) + ) + +const loadProfile = (userId: string) => + fetchProfile(userId).pipe( + Effect.retry({ + schedule: retryTransientProfileLoad, + while: isTransient + }), + Effect.tap((profile) => Console.log(`loaded ${profile.name}`)) + ) + +Effect.runPromise(loadProfile("user-123")).then(console.log, console.error) +// Output: +// load profile user-123, attempt 1 +// load profile user-123, attempt 2 +// load profile user-123, attempt 3 +// loaded Ada +// { id: 'user-123', name: 'Ada' } +``` + +##### Variants + +For a more latency-sensitive screen, reduce the cap to one or two retries. For a +less critical background refresh, increase the base delay and keep the cap +explicit. If the server returns a structured rate-limit response, classify it +separately instead of treating every `429` as an ordinary network failure. + +##### Notes and caveats + +`Schedule.recurs(3)` allows three retries after the first profile request. The +example uses tiny delays so it terminates quickly; use larger delays for a real +frontend path. `Schedule.jittered` randomly adjusts each delay between 80% and +120% of the base delay. + +Keep classification close to the HTTP boundary. The schedule should not parse +responses or guess whether a status is retryable; it should receive a domain +error and decide whether recurrence is still allowed. + +#### 27.3 Retry token refresh briefly + +Token refresh retries sit on an interactive path, so the policy must stay +narrow and brief. + +##### Problem + +An access token has expired and the client needs to exchange a refresh token +for a new access token. The refresh call can fail because the network timed out, +because the auth service returned a temporary `503`, or because the refresh +token is invalid, expired, revoked, or already rotated. + +Only the transient cases should be retried. Authentication failures must fail +fast so the client can sign the user out, ask for re-authentication, or follow +your product's session-recovery path. + +Use `Effect.retry` with a typed `while` predicate and a small finite `Schedule`. + +##### When to use it + +Use this recipe when token refresh is safe to attempt again and the caller can +tolerate a brief delay. It fits browser clients, mobile clients, and API +gateways that refresh credentials immediately before retrying the original +request. + +The refresh operation should have a clear error model. A specific +`RefreshTimeout` or `RefreshServiceUnavailable` tag is better than retrying every +failure from a generic HTTP client. + +##### When not to use it + +Do not retry `invalid_grant`, revoked-token, expired-token, malformed-request, +or client-authentication failures. Those are not made valid by waiting another +hundred milliseconds. + +Do not use a long backoff on an interactive token refresh path. If refresh is +still failing after a brief retry window, return control to the caller and let +the application decide whether to show an error, redirect to login, or continue +offline. + +Be careful with refresh-token rotation. If your identity provider treats a +duplicate refresh request as token reuse, retry only failures that are safe for +your provider, or use the provider's idempotency mechanism if it offers one. + +##### Schedule shape + +Start with a small exponential delay and bound it by both retry count and +elapsed time. + +`Schedule.exponential` chooses the next delay after an accepted failure. +`Schedule.jittered` randomizes each delay between 80% and 120% of the base +delay so many clients do not retry at exactly the same moment. + +`Schedule.recurs(2)` allows at most two retries after the original refresh. +`Schedule.during("1 second")` adds a short elapsed-time budget. Because +`Schedule.both` continues only while both schedules continue, retrying stops as +soon as either limit is exhausted. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +interface Tokens { + readonly accessToken: string + readonly refreshToken: string +} + +class RefreshTimeout extends Data.TaggedError("RefreshTimeout")<{ + readonly endpoint: string +}> {} + +class RefreshServiceUnavailable extends Data.TaggedError("RefreshServiceUnavailable")<{ + readonly endpoint: string +}> {} + +class RefreshRejected extends Data.TaggedError("RefreshRejected")<{ + readonly reason: "invalid_grant" | "revoked" | "expired" +}> {} + +type RefreshError = + | RefreshTimeout + | RefreshServiceUnavailable + | RefreshRejected + +let attempts = 0 + +const postRefreshToken = (refreshToken: string): Effect.Effect => + Effect.gen(function*() { + attempts += 1 + yield* Console.log(`refresh attempt ${attempts}`) + + if (refreshToken === "revoked") { + return yield* Effect.fail(new RefreshRejected({ reason: "revoked" })) + } + if (attempts === 1) { + return yield* Effect.fail(new RefreshTimeout({ endpoint: "/oauth/token" })) + } + + return { + accessToken: "access-token-2", + refreshToken: "refresh-token-2" + } + }) + +const isTransientRefreshFailure = ( + error: RefreshError +): error is RefreshTimeout | RefreshServiceUnavailable => + error._tag === "RefreshTimeout" || + error._tag === "RefreshServiceUnavailable" + +const retryTokenRefreshBriefly = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(2)), + Schedule.both(Schedule.during("150 millis")) +) + +const refreshSession = (refreshToken: string) => + postRefreshToken(refreshToken).pipe( + Effect.retry({ + schedule: retryTokenRefreshBriefly, + while: isTransientRefreshFailure + }), + Effect.tap((tokens) => Console.log(`new access token: ${tokens.accessToken}`)) + ) + +Effect.runPromise(refreshSession("refresh-token-1")).then(console.log, console.error) +// Output: +// refresh attempt 1 +// refresh attempt 2 +// new access token: access-token-2 +// { accessToken: 'access-token-2', refreshToken: 'refresh-token-2' } +``` + +`refreshSession` sends the refresh request once immediately. If the request +fails with `RefreshTimeout` or `RefreshServiceUnavailable`, `Effect.retry` +consults the schedule before trying again. + +If the provider rejects the token with `RefreshRejected`, the predicate returns +`false`, so the failure is returned without another refresh request. + +##### Variants + +For a very latency-sensitive path, retry once and use a smaller budget. + +For a backend-for-frontend or gateway where refresh does not block direct UI +interaction, you can allow a little more time while still keeping the policy +bounded. + +Keep the classification predicate separate from the schedule. The predicate +answers whether the failure is retryable; the schedule answers how brief and how +paced the retry window is. + +##### Notes and caveats + +`Effect.retry` retries typed failures from the effect's error channel. Defects +and interruptions are not turned into retryable token-refresh failures by the +schedule. + +The first refresh attempt is not delayed. Delays apply only after a failure has +been accepted by the `while` predicate. + +`Schedule.recurs(2)` means two retries after the original attempt, not two total +attempts. With the policy above, the client can make up to three refresh +requests total. + +Token refresh is security-sensitive. Keep retry brief, classify permanent +authentication failures explicitly, and verify the retry behavior against your +identity provider's refresh-token rotation rules. + +#### 27.4 Reconnect WebSocket with backoff + +WebSocket reconnect policies need to balance quick recovery with restraint +during real outages. + +##### Problem + +You have a browser or client application that opens a WebSocket connection. If +the connection cannot be opened because of a transient network failure, the +client should retry. Reconnecting immediately in a loop can create noisy UI and +extra load on the server, especially when many clients lose connectivity at the +same time. + +You want a reconnect policy that: + +- starts with a small delay +- grows after repeated failed opens +- caps the final wait so the UI remains understandable +- stops after a bounded number of retries + +##### When to use it + +Use this recipe for user-facing WebSocket reconnects where a temporary loss of +connectivity is expected: chat presence, live dashboards, collaborative editing, +notifications, subscriptions, and browser tabs that may move between networks. + +It is a good fit when the application can show intermediate states such as +"connecting", "reconnecting", and "offline" while the reconnect policy runs. + +##### When not to use it + +Do not retry permanent setup failures. Invalid URLs, unsupported protocols, +authentication failures, authorization failures, and application-level rejection +messages should be classified before the schedule is applied. + +Do not use the reconnect schedule as the only user experience. A person staring +at a disconnected screen needs visible state, a clear failure after the retry +budget is exhausted, and often a manual "try again" action. + +Do not use the schedule to supervise an already-open socket by itself. +`Effect.retry` retries failed effect evaluations. If a socket opens +successfully and later closes, model that close as a failure in the effect that +owns the connection lifecycle, then apply the reconnect policy around that +effect. + +##### Schedule shape + +Start with `Schedule.exponential`. It recurs forever by itself and doubles the +delay by default: 250 ms, 500 ms, 1 second, 2 seconds, and so on. + +For a user-facing reconnect, cap the final delay with `Schedule.modifyDelay` +and add a retry limit with `Schedule.recurs`. `Schedule.both` combines the +backoff and retry limit with intersection semantics, so reconnecting stops as +soon as the limit stops. + +##### Example + +```ts runnable deterministic +import { Console, Data, Duration, Effect, Schedule } from "effect" + +class WebSocketOpenError extends Data.TaggedError("WebSocketOpenError")<{ + readonly reason: "network" | "timeout" | "server-restarting" | "unauthorized" +}> {} + +interface LiveSocket { + readonly id: string +} + +let attempts = 0 + +const openLiveSocket: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`open WebSocket attempt ${attempts}`) + + if (attempts <= 2) { + return yield* Effect.fail(new WebSocketOpenError({ reason: "network" })) + } + + return { id: "live-socket-1" } +}) + +const isRetryableOpenError = (error: WebSocketOpenError): boolean => + error.reason === "network" || + error.reason === "timeout" || + error.reason === "server-restarting" + +const websocketReconnectPolicy = Schedule.exponential("10 millis").pipe( + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.millis(50)))), + Schedule.both(Schedule.recurs(8)) +) + +const connectLiveSocket = openLiveSocket.pipe( + Effect.retry({ + schedule: websocketReconnectPolicy, + while: isRetryableOpenError + }), + Effect.tap((socket) => Console.log(`connected ${socket.id}`)) +) + +Effect.runPromise(connectLiveSocket).then(console.log, console.error) +// Output: +// open WebSocket attempt 1 +// open WebSocket attempt 2 +// open WebSocket attempt 3 +// connected live-socket-1 +// { id: 'live-socket-1' } +``` + +`openLiveSocket` is evaluated once immediately. If opening the socket fails with +a `WebSocketOpenError`, `Effect.retry` feeds that failure into the schedule. The +schedule then decides whether another attempt is allowed and how long to wait +before trying again. + +##### Variants + +For a highly interactive screen, use fewer retries or a shorter cap. It is +usually better to show "offline" quickly and let the user retry manually than to +hide a long reconnect sequence behind a spinner. + +For a passive background tab or non-critical live feed, use a larger cap and a +larger retry budget. Keep the retry state observable so the UI can stop showing +stale data as if it were live. + +For large deployments, add jitter to this backoff. The cap protects the person +waiting in the UI; jitter protects the server from many clients retrying +together. Section 44.5 focuses on that version. + +For a socket that opens successfully and then closes later, wrap the whole +connection lifecycle in the effect being retried. The schedule should surround +the effect that can fail when the connection drops, not only the initial +constructor call. + +##### Notes and caveats + +The schedule controls delays between attempts. It does not time out a single +WebSocket opening attempt. If the open handshake can hang, add an effect-level +timeout to `openLiveSocket` before applying `Effect.retry`. + +The retry budget counts scheduled retries, not total connection attempts. +`Schedule.recurs(8)` means one immediate open attempt plus up to eight later +attempts. + +Be careful with authentication and authorization failures. Retrying a token that +is expired, missing, or forbidden usually makes the UI slower and the logs +noisier. Refresh credentials or ask the user to sign in before reconnecting. + +When the retry policy is exhausted, surface that state to the user. A bounded +WebSocket reconnect policy is only helpful if the application clearly moves from +"reconnecting" to "offline" or "connection lost" when the final attempt fails. + +#### 27.5 Reconnect WebSocket with jitter + +Jitter keeps WebSocket reconnect attempts from many clients from landing on the +same backoff boundaries. + +##### Problem + +A browser, mobile app, or frontend service owns a WebSocket connection. When the +socket closes for a transient reason, the client should reconnect without making +the user refresh the page. + +You want a reconnect policy that: + +- starts quickly for short network interruptions +- backs off after repeated failed reconnect attempts +- jitters each delay so many clients do not reconnect together +- caps each wait so the UI does not disappear into a long exponential tail +- stops after a bounded number of retries so the caller can surface a clear + disconnected state + +##### When to use it + +Use this recipe for reconnecting browser WebSockets, mobile realtime sessions, +dashboard event streams, collaborative editing channels, notification sockets, +and client-side presence connections. + +It is especially useful when many clients share the same gateway or realtime +backend. Jitter reduces the chance that a fleet of clients dropped by the same +event will all retry at the same 100 millisecond, 200 millisecond, 400 +millisecond, and 800 millisecond boundaries. + +Use it when reconnecting is safe and expected: the client can resubscribe, +refresh missed state, or resume from a known cursor after the socket is opened +again. + +##### When not to use it + +Do not retry authentication or authorization failures as if they were transient +socket failures. Expired credentials should refresh through the authentication +path; forbidden users should see the appropriate domain state. + +Do not use reconnect backoff as the only protection for the realtime service. +Gateways still need connection limits, admission control, heartbeats, and +server-side overload behavior. The schedule only controls when this client tries +again. + +Do not keep retrying forever in an interactive path without changing the user +state. After the retry budget is exhausted, surface that realtime updates are +disconnected and provide an explicit recovery path. + +##### Schedule shape + +Start with exponential backoff, add jitter, then clamp the final delay. + +`Schedule.exponential` starts with a short reconnect delay and doubles by +default. `Schedule.jittered` randomly adjusts each computed delay between 80% +and 120% of that delay. `Schedule.modifyDelay` applies the cap after jitter, so +the final sleep never exceeds the cap. + +`Schedule.recurs(8)` is the retry budget. With `Effect.retry`, the first +reconnect attempt runs immediately. If it fails, the schedule may allow up to +eight more attempts, each separated by the capped jittered backoff. + +##### Example + +```ts runnable deterministic +import { Console, Data, Duration, Effect, Schedule } from "effect" + +class WebSocketReconnectError extends Data.TaggedError("WebSocketReconnectError")<{ + readonly reason: "closed" | "timeout" | "gateway-unavailable" | "unauthorized" +}> {} + +let attempts = 0 + +const reconnectWebSocket: Effect.Effect = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`reconnect attempt ${attempts}`) + + if (attempts === 1) { + return yield* Effect.fail(new WebSocketReconnectError({ reason: "gateway-unavailable" })) + } + if (attempts === 2) { + return yield* Effect.fail(new WebSocketReconnectError({ reason: "timeout" })) + } + + return "socket-open" +}) + +const isRetryableReconnect = (error: WebSocketReconnectError) => + error.reason === "closed" || + error.reason === "timeout" || + error.reason === "gateway-unavailable" + +const webSocketReconnectPolicy = Schedule.exponential("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.jittered, + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.millis(50)))), + Schedule.both(Schedule.recurs(8)), + Schedule.while(({ input }) => isRetryableReconnect(input)) +) + +const program = reconnectWebSocket.pipe( + Effect.retry(webSocketReconnectPolicy), + Effect.tap((state) => Console.log(`connected: ${state}`)) +) + +Effect.runPromise(program).then(console.log, console.error) +// Output: +// reconnect attempt 1 +// reconnect attempt 2 +// reconnect attempt 3 +// connected: socket-open +// socket-open +``` + +`program` calls `reconnectWebSocket` once immediately. If the attempt fails with +`closed`, `timeout`, or `gateway-unavailable`, the first retry waits around 100 +milliseconds in a production-sized policy, adjusted by jitter. Later failures +use the exponential sequence as the base delay, then jitter and cap the final +sleep. The example uses 10 milliseconds so it terminates quickly. + +If the failure is `unauthorized`, the `Schedule.while` predicate stops retrying +immediately. If all permitted retries fail, `Effect.retry` returns the last +`WebSocketReconnectError`, and the UI can move to an explicit disconnected +state. + +##### Variants + +For a very latency-sensitive UI, lower the cap and retry count. This gives the +client a few fast attempts before asking the user to retry or showing a degraded +realtime state. + +For background clients, kiosks, or long-lived internal dashboards, use a larger +elapsed budget while keeping the per-delay cap. This lets the client keep +trying through a short outage without allowing any single sleep to grow beyond +the UI contract. + +##### Notes and caveats + +`Schedule.jittered` changes only delays. In Effect, it adjusts each delay between +`80%` and `120%` of the original delay. It does not classify errors, cap delays, +or decide how many retries are allowed. + +Apply the cap after jitter when the maximum sleep is part of the user-facing +contract. Without the final `Schedule.modifyDelay`, a jittered exponential delay +can still grow past the amount of time the UI is willing to wait silently. + +`Effect.retry` feeds the typed reconnect failure into the schedule. That is why +`Schedule.while` can stop retries for `unauthorized` while allowing transient +close, timeout, and gateway failures to use the reconnect policy. + +Across a large client population, jitter is a load-shaping tool, not just a +latency detail. The cap protects one client from waiting too long; jitter helps +the realtime backend avoid synchronized reconnect waves from many clients. + +### 28. Infrastructure and Platform Recipes + +#### 28.1 Retry dependency checks during startup + +Startup dependency checks sit between process boot and readiness. They can +absorb short platform races, but they should not hide configuration or schema +failures. + +##### Problem + +A service must prove that a required dependency is reachable before it marks +itself ready. DNS lookup failures, refused connections, and timeouts may clear +after a short wait. Bad credentials or schema mismatches should fail startup +immediately. + +Use one retry policy for the dependency check, not for the whole boot sequence. +The policy should slow repeated failures, cap the number of retries, and keep +the total startup wait bounded. + +##### When to use it + +Use this recipe for idempotent startup probes such as database connectivity, +cache reachability, message broker readiness, feature flag client +initialization, or a search cluster health check. + +It fits services that have not opened traffic yet and can afford a short +readiness delay while still giving operators a clear failure when the dependency +does not recover. + +##### When not to use it + +Do not retry permanent startup failures. Missing secrets, bad credentials, +invalid endpoints, incompatible schema versions, and malformed configuration +should fail startup immediately. + +Do not put the whole boot sequence inside the retry. Keep the retry boundary +around the small dependency check. Initialization steps that create records, +run migrations, or perform writes need their own idempotency guarantees before +they are retried. + +##### Schedule shape + +Start with `Schedule.exponential` for backoff. It does not stop by itself, so +combine it with `Schedule.recurs` for the retry count and `Schedule.during` for +the elapsed startup budget. + +Use `Schedule.modifyDelay` with `Duration.min` when no individual sleep should +grow beyond a maximum. `Schedule.both` keeps the policy running only while both +sides still allow another retry; the backoff supplies the delay, and the count +and time schedules supply stopping conditions. + +##### Example + +```ts runnable deterministic +import { Console, Data, Duration, Effect, Schedule } from "effect" + +class DependencyCheckError extends Data.TaggedError("DependencyCheckError")<{ + readonly reason: + | "DnsLookup" + | "ConnectionRefused" + | "Timeout" + | "BadCredentials" + | "SchemaMismatch" +}> {} + +let attempts = 0 + +const checkDatabase = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`database check ${attempts}`) + + if (attempts === 1) { + return yield* Effect.fail(new DependencyCheckError({ reason: "DnsLookup" })) + } + if (attempts === 2) { + return yield* Effect.fail(new DependencyCheckError({ reason: "Timeout" })) + } + + yield* Console.log("database reachable") +}) + +const isRetryableStartupFailure = (error: DependencyCheckError) => + error.reason === "DnsLookup" || + error.reason === "ConnectionRefused" || + error.reason === "Timeout" + +const startupDependencyPolicy = Schedule.exponential("10 millis").pipe( + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.millis(30)))), + Schedule.both(Schedule.recurs(5)), + Schedule.both(Schedule.during("200 millis")) +) + +const program = checkDatabase.pipe( + Effect.retry({ schedule: startupDependencyPolicy, while: isRetryableStartupFailure }), + Effect.flatMap(() => Console.log(`startup ready after ${attempts} checks`)), + Effect.catch((error: DependencyCheckError) => Console.log(`startup failed: ${error.reason}`)) +) + +void Effect.runPromise(program) +// Output: +// database check 1 +// database check 2 +// database check 3 +// database reachable +// startup ready after 3 checks +``` + +The demo runs quickly by using millisecond delays. In production, use larger +values that match the orchestrator's readiness budget. The first dependency +check runs immediately; only follow-up attempts are scheduled. + +##### Variants + +For a stricter container readiness path, reduce both the deadline and retry +count. For dependencies that commonly take longer during deploys, keep the first +retry quick but allow a longer total budget. If many instances start at the same +time, add `Schedule.jittered` before the delay cap so they do not retry on the +same boundaries. + +##### Notes and caveats + +`Effect.retry` feeds each typed failure into the schedule after the effect +fails. The original startup check is not delayed. + +`Schedule.exponential` controls the waits between retries. It is not a total +timeout. Pair it with `Schedule.recurs` and `Schedule.during` when startup must +either become ready or fail within a known budget. + +The deadline here is a schedule deadline, not a timeout for a single check. If +one dependency check can hang, put an Effect timeout on that check before +retrying it. + +#### 28.2 Poll until all required services are ready + +Startup readiness polling coordinates several platform services before traffic +opens. Keep readiness classification in domain code; let the schedule decide +when another successful observation is allowed. + +##### Problem + +At boot, the service reads a readiness snapshot for the database, broker, and +cache. It should keep polling while any required service is still starting, stop +immediately if a required service reports failure, and return a timeout result +if the startup budget expires. + +##### When to use it + +Use this recipe for boot-time coordination where readiness is eventually +consistent and a short wait is normal. It fits container startup, deployment +hooks, worker initialization, and control-plane checks where the caller needs +one final answer: ready, failed, or timed out. + +##### When not to use it + +Do not poll when the dependency provides a reliable startup event, health +stream, or orchestration signal. Also avoid this shape when a failed dependency +should not block the process; start in degraded mode and monitor readiness +separately. + +##### Schedule shape + +Use a spaced cadence, pass each successful readiness snapshot through as the +schedule output, and stop recurring once the latest snapshot is terminal. Add a +budget so startup cannot wait indefinitely. + +The first readiness check happens before the schedule decides whether to recur. +The schedule controls only the follow-up checks. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +type ServiceName = "database" | "broker" | "cache" + +type ServiceReadiness = + | { readonly _tag: "Ready"; readonly service: ServiceName } + | { readonly _tag: "Starting"; readonly service: ServiceName } + | { readonly _tag: "Failed"; readonly service: ServiceName; readonly reason: string } + +type FailedServiceReadiness = Extract + +interface ReadinessSnapshot { + readonly services: ReadonlyArray +} + +class ReadinessCheckError extends Data.TaggedError("ReadinessCheckError")<{ + readonly reason: string +}> {} + +class StartupDependencyFailed extends Data.TaggedError("StartupDependencyFailed")<{ + readonly failed: ReadonlyArray +}> {} + +class StartupReadinessTimedOut extends Data.TaggedError("StartupReadinessTimedOut")<{ + readonly latest: ReadinessSnapshot +}> {} + +const snapshots: ReadonlyArray = [ + { + services: [ + { _tag: "Starting", service: "database" }, + { _tag: "Starting", service: "broker" }, + { _tag: "Ready", service: "cache" } + ] + }, + { + services: [ + { _tag: "Ready", service: "database" }, + { _tag: "Starting", service: "broker" }, + { _tag: "Ready", service: "cache" } + ] + }, + { + services: [ + { _tag: "Ready", service: "database" }, + { _tag: "Ready", service: "broker" }, + { _tag: "Ready", service: "cache" } + ] + } +] + +let reads = 0 + +const readPlatformReadiness = Effect.gen(function*() { + const snapshot = snapshots[Math.min(reads, snapshots.length - 1)] + reads += 1 + const summary = snapshot.services + .map((service) => `${service.service}:${service._tag}`) + .join(", ") + yield* Console.log( + `readiness ${reads}: ${summary}` + ) + return snapshot +}) + +const allReady = (snapshot: ReadinessSnapshot) => snapshot.services.every((service) => service._tag === "Ready") + +const failedServices = (snapshot: ReadinessSnapshot) => + snapshot.services.filter( + (service): service is FailedServiceReadiness => service._tag === "Failed" + ) + +const isTerminal = (snapshot: ReadinessSnapshot) => allReady(snapshot) || failedServices(snapshot).length > 0 + +const readinessPolling = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => !isTerminal(input)), + Schedule.bothLeft(Schedule.during("200 millis")), + Schedule.bothLeft(Schedule.recurs(5)) +) + +const waitForRequiredServices = Effect.gen(function*() { + const latest = yield* Effect.repeat(readPlatformReadiness, readinessPolling) + const failed = failedServices(latest) + + if (failed.length > 0) { + return yield* Effect.fail(new StartupDependencyFailed({ failed })) + } + + if (!allReady(latest)) { + return yield* Effect.fail(new StartupReadinessTimedOut({ latest })) + } + + return latest +}) + +const program = waitForRequiredServices.pipe( + Effect.flatMap(() => Console.log("all required services are ready")), + Effect.catch((error) => Console.log(`startup stopped: ${error._tag}`)) +) + +void Effect.runPromise(program) +// Output: +// readiness 1: database:Starting, broker:Starting, cache:Ready +// readiness 2: database:Ready, broker:Starting, cache:Ready +// readiness 3: database:Ready, broker:Ready, cache:Ready +// all required services are ready +``` + +##### Variants + +For a single instance startup path, a short fixed cadence is usually enough. For +a large fleet, add jitter after choosing the base cadence so instances do not +poll the same platform APIs at the same time. For slow infrastructure, increase +the budget deliberately rather than leaving the schedule unbounded. + +If readiness reads themselves can fail transiently, handle that separately from +terminal service state. Retry `readPlatformReadiness` on transport errors with a +small retry policy, then repeat successful snapshots with the readiness polling +policy. + +##### Notes and caveats + +`Effect.repeat` feeds successful values into the schedule, so +`Schedule.passthrough` lets the predicate inspect the latest +`ReadinessSnapshot`. The final check after `Effect.repeat` is still necessary: +the schedule can stop because every service is ready, because a service failed, +or because the budget was exhausted. + +`Schedule.during` is a budget for recurrence decisions, not a replacement for +domain classification. Keep terminal states explicit so operators can +distinguish "dependency failed" from "startup waited long enough." + +#### 28.3 Poll rollout status + +Rollout polling turns an external deployment's progress into a bounded wait. +The status endpoint reports domain states as successful responses, so the +schedule should inspect those values rather than treat unfinished work as an +error. + +##### Problem + +A deploy controller has a rollout id and needs one final outcome: succeeded, +failed, or still running when the polling budget expires. While the latest +status is `"running"`, it should wait and read again. + +Keep read failures separate from rollout failures. A timeout or malformed +response from the status endpoint belongs in the Effect error channel. A rollout +status of `"failed"` is a successful read that stops polling just like +`"succeeded"` does. + +##### When to use it + +Use this recipe when the rollout API exposes terminal domain states and callers +need to distinguish all three outcomes: + +- the rollout is still `"running"` when the polling budget is exhausted +- the rollout finished with `"succeeded"` +- the rollout finished with `"failed"` + +This is a good fit for deployment controllers, progressive delivery systems, +schema migrations, feature-flag rollouts, and infrastructure provisioning where +the operation continues outside the current process. + +##### When not to use it + +Do not use this as a retry policy for failed status reads. With `Effect.repeat`, +a failed read stops the repeat before the schedule can inspect a status. If +transient reads should be retried, add a separate `Effect.retry` around the +single status read. + +Do not encode a rollout's terminal `"failed"` status as an Effect failure just +to stop polling. Keep it as a successful status value, stop the schedule, and +decide what it means after polling completes. + +Do not leave a fleet-wide polling policy unbounded. Add an elapsed budget, +recurrence limit, or owner fiber that can interrupt the poller. + +##### Schedule shape + +Start with a cadence, add jitter for fleet-wide polling, pass the latest status +through as the schedule output, and continue only while the latest status is +running. Add a count or elapsed budget so a rollout that never becomes terminal +does not poll forever. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type RolloutStatus = + | { + readonly state: "running" + readonly rolloutId: string + readonly completedInstances: number + readonly totalInstances: number + } + | { + readonly state: "succeeded" + readonly rolloutId: string + readonly version: string + } + | { + readonly state: "failed" + readonly rolloutId: string + readonly reason: string + } + +type StatusReadError = { + readonly _tag: "StatusReadError" + readonly rolloutId: string +} + +type RolloutTimedOut = { + readonly _tag: "RolloutTimedOut" + readonly lastStatus: Extract +} + +type RolloutFailed = { + readonly _tag: "RolloutFailed" + readonly rolloutId: string + readonly reason: string +} + +const statuses: ReadonlyArray = [ + { + state: "running", + rolloutId: "rollout-42", + completedInstances: 1, + totalInstances: 3 + }, + { + state: "running", + rolloutId: "rollout-42", + completedInstances: 2, + totalInstances: 3 + }, + { + state: "succeeded", + rolloutId: "rollout-42", + version: "2026.05.17" + } +] + +let reads = 0 + +const readRolloutStatus: (rolloutId: string) => Effect.Effect = Effect.fnUntraced( + function*(rolloutId: string) { + const status = statuses[Math.min(reads, statuses.length - 1)] + reads += 1 + yield* Console.log(`rollout read ${reads} for ${rolloutId}: ${status.state}`) + return status + } +) + +const pollRolloutStatus = Schedule.spaced("10 millis").pipe( + Schedule.jittered, + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "running"), + Schedule.bothLeft(Schedule.during("200 millis")), + Schedule.bothLeft(Schedule.recurs(5)) +) + +const waitForRollout = (rolloutId: string) => + readRolloutStatus(rolloutId).pipe( + Effect.repeat(pollRolloutStatus), + Effect.flatMap((status): Effect.Effect< + Extract, + RolloutFailed | RolloutTimedOut + > => { + switch (status.state) { + case "succeeded": + return Effect.succeed(status) + case "failed": + return Effect.fail( + { + _tag: "RolloutFailed", + rolloutId: status.rolloutId, + reason: status.reason + } satisfies RolloutFailed + ) + case "running": + return Effect.fail( + { + _tag: "RolloutTimedOut", + lastStatus: status + } satisfies RolloutTimedOut + ) + } + }) + ) + +const program = waitForRollout("rollout-42").pipe( + Effect.flatMap((status) => Console.log(`rollout ${status.rolloutId} finished on ${status.version}`)), + Effect.catch((error: RolloutFailed | RolloutTimedOut | StatusReadError) => + Console.log(`rollout stopped: ${error._tag}`) + ) +) + +void Effect.runPromise(program) +// Output: +// rollout read 1 for rollout-42: running +// rollout read 2 for rollout-42: running +// rollout read 3 for rollout-42: succeeded +// rollout rollout-42 finished on 2026.05.17 +``` + +`waitForRollout` reads immediately. If the first result is `"succeeded"` or +`"failed"`, there is no delay and no second request. If the result is +`"running"`, the schedule waits, applies jitter, and reads again. + +The repeat returns the last observed `RolloutStatus`. The final `flatMap` keeps +the three outcomes separate: success returns the succeeded status, rollout +failure becomes `RolloutFailed`, and exhausting the polling budget while still +running becomes `RolloutTimedOut`. + +##### Variants + +For a command-line tool, use a smaller budget. For a background reconciler, use +a slower cadence and a recurrence cap when each read already has its own request +timeout. For transient status-read failures, retry the read itself and then +repeat successful statuses: + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type StatusReadError = { readonly _tag: "StatusReadError" } +type RolloutStatus = { readonly state: "running" | "succeeded" } + +let attempts = 0 + +const readStatus = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`status read attempt ${attempts}`) + if (attempts === 1) { + return yield* Effect.fail({ _tag: "StatusReadError" } satisfies StatusReadError) + } + return { state: attempts < 3 ? "running" : "succeeded" } satisfies RolloutStatus +}) + +const readRetry = Schedule.exponential("10 millis").pipe( + Schedule.both(Schedule.recurs(2)) +) + +const pollStatus = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.state === "running"), + Schedule.bothLeft(Schedule.recurs(4)) +) + +const program = readStatus.pipe( + Effect.retry(readRetry), + Effect.repeat(pollStatus), + Effect.flatMap((status) => Console.log(`final status: ${status.state}`)), + Effect.catch((error: StatusReadError) => Console.log(`status read failed: ${error._tag}`)) +) + +void Effect.runPromise(program) +// Output: +// status read attempt 1 +// status read attempt 2 +// status read attempt 3 +// final status: succeeded +``` + +The retry schedule sees status-read errors. The repeat schedule sees successful +`RolloutStatus` values. + +##### Notes and caveats + +The first status read is not delayed. Schedule delays apply only before later +recurrences. + +`Schedule.while` is evaluated after a successful status read. It does not cancel +a read that is already in progress. + +`Schedule.during` limits recurrence decisions. If the budget is exhausted before +a terminal status is observed, `Effect.repeat` still returns the last successful +status, which can be `"running"`. + +Use `Schedule.passthrough` when the caller needs the final domain status. If you +omit it, the repeat returns the timing schedule's output instead of the rollout +status. + +#### 28.4 Retry deployment hooks + +Deployment hooks bridge a deploy system and external platform services. They +can be retried only when the hook call has a duplicate-safe contract and the +retry policy is bounded. + +##### Problem + +After a deployment reaches the point where post-deploy hooks should run, the +hook endpoint sometimes returns a timeout, `429`, or `503`. The hook operation is +idempotent because the request includes a stable deployment id and hook id, so +the receiver can collapse duplicates. + +The retry policy should run the first call immediately, back off after failures, +add jitter for fleet-wide deploys, cap the delay, stop after a small number of +retries, and retry only transient hook failures. + +##### When to use it + +Use this recipe for deployment hooks that have an explicit duplicate-suppression +boundary: an idempotency key, a natural resource identity, or a receiver-side +record keyed by deployment and hook name. + +It fits post-deploy notifications, audit writes, cache purge requests, smoke +test triggers, and control-plane updates where a retry can reasonably succeed +after a short outage or rate-limit window. + +##### When not to use it + +Do not retry a hook that is not idempotent. If the receiver creates a ticket, +sends a page, advances a workflow, or mutates deployment state without a +duplicate key, retrying can perform the action more than once. + +Do not retry permanent errors. Bad hook configuration, missing credentials, +forbidden access, unknown deployment ids, and validation failures should surface +as deployment problems instead of being hidden behind backoff. + +##### Schedule shape + +Use exponential backoff with jitter, cap each sleep, and combine it with +`Schedule.recurs`. `Effect.retry` feeds typed failures into the schedule, so a +`while` predicate can stop immediately for non-retryable hook errors. + +##### Example + +```ts runnable deterministic +import { Console, Data, Duration, Effect, Schedule } from "effect" + +class DeploymentHookError extends Data.TaggedError("DeploymentHookError")<{ + readonly status: number + readonly message: string +}> {} + +interface HookReceipt { + readonly deploymentId: string + readonly hookName: string + readonly accepted: boolean +} + +let attempts = 0 + +const invokeDeploymentHook: (request: { + readonly deploymentId: string + readonly hookName: string + readonly idempotencyKey: string +}) => Effect.Effect = Effect.fnUntraced(function*(request) { + attempts += 1 + yield* Console.log(`hook attempt ${attempts}: ${request.hookName}`) + + if (attempts === 1) { + return yield* Effect.fail( + new DeploymentHookError({ + status: 503, + message: "hook receiver unavailable" + }) + ) + } + if (attempts === 2) { + return yield* Effect.fail( + new DeploymentHookError({ + status: 429, + message: "hook receiver is throttling" + }) + ) + } + + return { + deploymentId: request.deploymentId, + hookName: request.hookName, + accepted: true + } satisfies HookReceipt +}) + +const isRetryableHookError = (error: DeploymentHookError) => + error.status === 408 || + error.status === 409 || + error.status === 429 || + error.status >= 500 + +const deploymentHookRetryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.jittered, + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.millis(40)))), + Schedule.both(Schedule.recurs(5)), + Schedule.while(({ input }) => isRetryableHookError(input)) +) + +const program = invokeDeploymentHook({ + deploymentId: "deploy-2026-05-16-001", + hookName: "post-deploy-smoke-test", + idempotencyKey: "deploy-2026-05-16-001:post-deploy-smoke-test" +}).pipe( + Effect.retry(deploymentHookRetryPolicy), + Effect.flatMap((receipt) => Console.log(`hook accepted: ${receipt.deploymentId}/${receipt.hookName}`)), + Effect.catch((error: DeploymentHookError) => Console.log(`hook failed with status ${error.status}: ${error.message}`)) +) + +void Effect.runPromise(program) +// Output: +// hook attempt 1: post-deploy-smoke-test +// hook attempt 2: post-deploy-smoke-test +// hook attempt 3: post-deploy-smoke-test +// hook accepted: deploy-2026-05-16-001/post-deploy-smoke-test +``` + +##### Variants + +For a latency-sensitive deploy gate, reduce the retry count or combine the +policy with `Schedule.during` so the deployment controller gets a clear failure +within its rollout budget. + +For a best-effort notification hook, keep the deployment path short and enqueue +the hook for a worker that can use a longer retry policy outside the critical +rollout path. + +For hooks called by many services at once, keep jitter enabled even when the +maximum delay is low. Backoff reduces pressure after repeated failures; jitter +reduces synchronization across callers. + +##### Notes and caveats + +The original hook call is not counted by `Schedule.recurs(5)`. The schedule +controls only follow-up retries after a failure, so this policy permits one +initial call plus at most five retries. + +`Schedule.exponential` has no retry limit by itself. Always pair it with +`Schedule.recurs`, `Schedule.take`, `Schedule.during`, or a predicate that stops +when retrying no longer makes sense. + +Backoff does not make a hook safe to retry. The safety boundary must come from +the hook protocol: a stable idempotency key, deterministic operation identity, +or receiver-side deduplication. + +`Effect.retry` feeds `DeploymentHookError` values into the schedule. That is why +`Schedule.while` can classify retryable statuses without mixing sleeps and +counters into the hook implementation. + +#### 28.5 Retry infrastructure API calls + +Infrastructure retries happen against shared control-plane capacity. A useful +retry policy gives transient failures time to clear without turning automation +into a traffic spike. + +##### Problem + +A provisioning worker calls a provider API to create a subnet. The request may +time out, receive a `503`, or hit a `429`; invalid requests and unsafe writes +should leave the retry path immediately. + +Retrying immediately can make the incident worse. Retrying forever can hold a +worker or deployment open past its useful deadline. Retrying an unsafe write can +duplicate side effects. Model retryable infrastructure failures explicitly and +put the recurrence policy in one named schedule. + +##### When to use it + +Use this recipe when the operation is safe to attempt more than once and the +failure is plausibly temporary. Good examples include reading instance status, +refreshing a load balancer target, creating a resource with an idempotency key, +or applying the same desired state to the same resource identifier. + +It is especially useful for platform workers that may run in parallel. Backoff +reduces pressure on a struggling dependency, jitter prevents many workers from +retrying in the same millisecond, and a time budget gives operators a clear +deadline for when the retry window closes. + +##### When not to use it + +Do not retry authorization failures, malformed requests, missing tenants, +invalid resource names, or other permanent errors. Those failures should leave +the retry path immediately. + +Do not retry non-idempotent writes unless the API gives you a duplicate-safe +contract, such as an idempotency key, a stable client token, an upsert by +resource name, or a documented "set desired state" endpoint. + +Do not treat `429 Too Many Requests` like an ordinary `503`. A rate limit is +feedback from the provider that this caller should slow down. Preserve +`Retry-After` or quota metadata when the API gives it to you. + +##### Schedule shape + +Start with exponential backoff, add jitter, and combine it with both a retry +count and an elapsed retry budget. `Schedule.both` continues only while both +sides continue and uses the maximum of their delays; the backoff side supplies +the waits while the count and duration schedules supply stopping conditions. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class ApiTimeout extends Data.TaggedError("ApiTimeout")<{ + readonly operation: "CreateSubnet" +}> {} + +class ApiUnavailable extends Data.TaggedError("ApiUnavailable")<{ + readonly status: 502 | 503 | 504 +}> {} + +class RateLimited extends Data.TaggedError("RateLimited")<{ + readonly retryAfterMillis?: number +}> {} + +class InvalidRequest extends Data.TaggedError("InvalidRequest")<{ + readonly reason: string +}> {} + +type InfrastructureApiError = + | ApiTimeout + | ApiUnavailable + | RateLimited + | InvalidRequest + +let attempts = 0 + +const createSubnet: (request: { + readonly vpcId: string + readonly cidrBlock: string + readonly clientToken: string +}) => Effect.Effect = Effect.fnUntraced(function*(request) { + attempts += 1 + yield* Console.log(`create subnet attempt ${attempts} with ${request.clientToken}`) + + if (attempts === 1) { + return yield* Effect.fail(new ApiTimeout({ operation: "CreateSubnet" })) + } + if (attempts === 2) { + return yield* Effect.fail(new ApiUnavailable({ status: 503 })) + } + + return `subnet-${request.cidrBlock}` +}) + +const retryInfrastructureApi = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(5)), + Schedule.both(Schedule.during("200 millis")) +) + +const program = createSubnet({ + vpcId: "vpc-123", + cidrBlock: "10.0.8.0/24", + clientToken: "deploy-2026-05-16-subnet-10-0-8" +}).pipe( + Effect.retry({ + schedule: retryInfrastructureApi, + while: (error) => + error._tag === "ApiTimeout" || + error._tag === "ApiUnavailable" || + error._tag === "RateLimited" + }), + Effect.flatMap((subnetId) => Console.log(`created ${subnetId}`)), + Effect.catch((error: InfrastructureApiError) => Console.log(`infrastructure call failed: ${error._tag}`)) +) + +void Effect.runPromise(program) +// Output: +// create subnet attempt 1 with deploy-2026-05-16-subnet-10-0-8 +// create subnet attempt 2 with deploy-2026-05-16-subnet-10-0-8 +// create subnet attempt 3 with deploy-2026-05-16-subnet-10-0-8 +// created subnet-10.0.8.0/24 +``` + +The `clientToken` is the idempotency guard. If the first request reached the +provider but the response was lost, the retry represents the same logical +operation rather than a second independent subnet creation. + +`InvalidRequest` is deliberately excluded from the retry predicate. Repeating +the same malformed request would only spend retry budget and add control-plane +traffic. + +##### Variants + +When a rate-limit response includes provider guidance, keep that information in +the typed error and prefer the provider's `Retry-After` timing over a guessed +delay. For background reconciliation, use a larger budget but keep the same +shape. For an interactive deployment command, shorten the budget so the caller +gets a clear result quickly. + +##### Notes and caveats + +`Effect.retry` feeds typed failures into the schedule after an attempt fails. +The first infrastructure API call is not delayed, and defects or interruptions +are not treated as typed retry failures. + +`Schedule.during` is a retry-window budget, not a hard deadline for an +individual HTTP request. If each API attempt also needs its own timeout, put +that timeout around the API call itself and then retry the resulting typed +timeout failure. + +Backoff and jitter protect the provider, but they do not make a write safe. +Idempotency is a property of the API request and the provider contract. Use a +stable client token, resource name, deduplication key, or "set desired state" +operation before retrying writes. + +Keep rate-limit handling explicit. A generic jittered backoff is acceptable +when no retry hint exists, but provider guidance such as `Retry-After` should +usually win over a guessed delay. + +### 29. Data and Batch Recipes + +#### 29.1 Poll ETL status until completion + +ETL polling observes work that continues in a data platform after submission. +Treat status responses as domain values; let the schedule decide when to ask +again and let surrounding Effect code interpret the final state. + +##### Problem + +A caller has an ETL run id and needs the last observed status: terminal if the +run finishes, or still active when the polling window expires. Active states +should be spaced out with waits between reads instead of a tight loop. + +The status check itself can also hang or fail. That is a separate concern from +the polling schedule: use an operation timeout for each status read, and use a +schedule budget for the overall recurrence window. + +##### When to use it + +Use this when the ETL platform exposes completion as a status endpoint and the +non-terminal statuses are normal successful values. + +This is a good fit for batch imports, warehouse loads, dbt or Spark jobs, +materialized-view refreshes, and vendor APIs where completion is observed by +polling a run id. + +##### When not to use it + +Do not use this to hide a broken status endpoint. With `Effect.repeat`, a +failure from the status read stops polling. Add a separate retry policy around +the status read only when transport or decoding failures are transient and safe +to retry. + +Do not turn ETL terminal states such as `"failed"` or `"canceled"` into effect +failures inside the polling loop unless every caller wants that behavior. It is +usually clearer to poll until a terminal status is observed, then map the final +status into the caller's domain result. + +Do not treat `Schedule.during` as a hard timeout for an in-flight HTTP request. +It is evaluated at recurrence decision points. Use `Effect.timeout` on the +status read when each request needs its own interruption limit. + +##### Schedule shape + +Combine a polling cadence, a terminal-state predicate, and an elapsed recurrence +budget. `Schedule.spaced` waits after each completed status read before the next +poll. `Schedule.while` continues only while the latest successful status is +non-terminal. `Schedule.during` bounds the recurrence window. + +`Schedule.passthrough` makes the schedule output the latest `EtlStatus`, and +`Schedule.bothLeft` preserves that output after composing the elapsed budget. +The repeated effect therefore returns the final observed ETL status, not the +schedule's timing or count output. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type EtlStatus = + | { readonly state: "queued" } + | { readonly state: "extracting"; readonly rowsRead: number } + | { readonly state: "loading"; readonly rowsWritten: number } + | { readonly state: "succeeded"; readonly outputTable: string } + | { readonly state: "failed"; readonly reason: string } + | { readonly state: "canceled" } + +type StatusReadError = { + readonly _tag: "StatusReadError" + readonly message: string +} + +const isTerminal = (status: EtlStatus): boolean => + status.state === "succeeded" || + status.state === "failed" || + status.state === "canceled" + +const statuses: ReadonlyArray = [ + { state: "queued" }, + { state: "extracting", rowsRead: 1_000 }, + { state: "loading", rowsWritten: 1_000 }, + { state: "succeeded", outputTable: "analytics.daily_orders" } +] + +let reads = 0 + +const readEtlStatus: (runId: string) => Effect.Effect = Effect.fnUntraced( + function*(runId: string) { + const status = statuses[Math.min(reads, statuses.length - 1)] + reads += 1 + yield* Console.log(`ETL ${runId} read ${reads}: ${status.state}`) + return status + } +) + +const pollEtlStatusBudget = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => !isTerminal(input)), + Schedule.bothLeft(Schedule.during("300 millis")), + Schedule.bothLeft(Schedule.recurs(8)) +) + +const pollEtlStatus = (runId: string) => + readEtlStatus(runId).pipe( + Effect.timeout("50 millis"), + Effect.repeat(pollEtlStatusBudget) + ) + +const program = pollEtlStatus("etl-run-7").pipe( + Effect.flatMap((status) => + status.state === "succeeded" + ? Console.log(`ETL completed: ${status.outputTable}`) + : Console.log(`ETL stopped while ${status.state}`) + ), + Effect.catch((error) => Console.log(`ETL status read failed: ${String(error)}`)) +) + +void Effect.runPromise(program) +// Output: +// ETL etl-run-7 read 1: queued +// ETL etl-run-7 read 2: extracting +// ETL etl-run-7 read 3: loading +// ETL etl-run-7 read 4: succeeded +// ETL completed: analytics.daily_orders +``` + +`pollEtlStatus` performs the first status read immediately. If the first +successful response is `"succeeded"`, `"failed"`, or `"canceled"`, polling stops +without waiting. If the response is still active, the schedule waits before the +next read and continues while the recurrence budget allows another poll. + +The effect succeeds with the last observed `EtlStatus`. That status may be +terminal, or it may still be active if the schedule budget stopped allowing +further recurrences. The effect fails only if a status read fails or if a +per-read timeout interrupts a status read. + +##### Variants + +For a user-facing request, shorten both limits: a one-second status-read timeout +and a 30-second recurrence budget often make more sense than a long batch +worker budget. + +For a background reconciler, increase the spacing and add `Schedule.jittered` +after the basic policy is correct so many workers do not poll the ETL control +plane at the same instant. + +If the caller must fail when the ETL run ends in `"failed"` or `"canceled"`, +keep that decision after polling. This keeps polling mechanics separate from the +business rule for incomplete or unsuccessful ETL runs. + +##### Notes and caveats + +The first status read is not delayed. The schedule controls only recurrences +after a successful status read. + +`Effect.repeat` feeds successful `EtlStatus` values into the schedule. Failed +status reads do not become schedule inputs. + +`Schedule.during` is a recurrence budget, not a hard deadline for the whole +program. `Effect.timeout` is the per-read timeout in this example. + +When a timing schedule reads the latest status through `metadata.input`, +constrain the schedule with `Schedule.satisfiesInputType()` before +using `Schedule.while`. + +#### 29.2 Retry export generation + +Export retries spend real batch capacity, so the policy should be conservative. +Keep transient-error classification and the retry limit visible next to the +generation call. + +##### Problem + +A report, invoice bundle, or customer data export may fail because the database +is temporarily unavailable, the renderer is saturated, or object storage is +down. Invalid requests and permission failures should surface immediately. + +The retry schedule should recover from short outages without regenerating large +exports indefinitely. + +##### When to use it + +Use this recipe when export generation is idempotent or protected by an export +job id, so a repeated attempt resumes or replaces the same logical export rather +than creating duplicate user-visible artifacts. It is also a good fit when the +caller can wait a short time for recovery, but the system must not keep retrying +large batch work indefinitely. + +##### When not to use it + +Do not retry malformed filters, missing authorization, unsupported formats, or +other permanent request problems. Do not use this policy for non-idempotent +exports that create a new billable artifact on every attempt unless the +generation layer has a deduplication key. + +##### Schedule shape + +Use `Effect.retry` because export generation is retried after failures. The +retry options receive each failure in `while`, so the classifier can stop the +retry loop for permanent errors. Combine that classifier with bounded backoff +and jitter so export workers do not retry in lockstep. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type ExportRequest = { + readonly exportId: string + readonly accountId: string + readonly format: "csv" | "parquet" +} + +type ExportFile = { + readonly exportId: string + readonly location: string +} + +type ExportError = + | { readonly _tag: "DatabaseUnavailable" } + | { readonly _tag: "RendererBusy" } + | { readonly _tag: "ObjectStorageUnavailable" } + | { readonly _tag: "InvalidExportRequest"; readonly reason: string } + | { readonly _tag: "PermissionDenied" } + +let attempts = 0 + +const generateExport: (request: ExportRequest) => Effect.Effect = Effect.fnUntraced( + function*(request: ExportRequest) { + attempts += 1 + yield* Console.log(`export attempt ${attempts}: ${request.exportId}`) + + if (attempts === 1) { + return yield* Effect.fail({ _tag: "RendererBusy" } satisfies ExportError) + } + if (attempts === 2) { + return yield* Effect.fail({ _tag: "ObjectStorageUnavailable" } satisfies ExportError) + } + + return { + exportId: request.exportId, + location: `s3://exports/${request.accountId}/${request.exportId}.${request.format}` + } + } +) + +const isTransientExportError = (error: ExportError): boolean => { + switch (error._tag) { + case "DatabaseUnavailable": + case "RendererBusy": + case "ObjectStorageUnavailable": + return true + case "InvalidExportRequest": + case "PermissionDenied": + return false + } +} + +const retryTransientExportFailures = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(4)) +) + +const runExport = (request: ExportRequest) => + generateExport(request).pipe( + Effect.retry({ + schedule: retryTransientExportFailures, + while: isTransientExportError + }) + ) + +const program = runExport({ + exportId: "export-2026-05-17", + accountId: "acct-123", + format: "csv" +}).pipe( + Effect.flatMap((file) => Console.log(`export ready: ${file.location}`)), + Effect.catch((error: ExportError) => Console.log(`export failed: ${error._tag}`)) +) + +void Effect.runPromise(program) +// Output: +// export attempt 1: export-2026-05-17 +// export attempt 2: export-2026-05-17 +// export attempt 3: export-2026-05-17 +// export ready: s3://exports/acct-123/export-2026-05-17.csv +``` + +##### Variants + +For an interactive download, reduce the retry count or add +`Schedule.during("10 seconds")` so the caller gets a timely failure. For a +background export queue, use a larger job-level timeout outside the retry policy +and keep this schedule focused on short transient recovery. For a large worker +fleet, keep `Schedule.jittered`; synchronized retries from many failed exports +can become a second incident. + +##### Notes and caveats + +`Schedule.recurs(4)` allows at most four retries after the first generation +attempt. The `while` predicate passed to `Effect.retry` inspects each typed +failure; when the error is permanent, retrying stops and the original export +error is returned. Keep the classifier conservative. It is better to surface a +permanent export failure than to repeatedly regenerate a large file that can +never succeed. + +#### 29.3 Retry file upload to object storage + +Object storage uploads are retryable only when duplicate attempts are harmless. +The upload protocol supplies the stable identity; the schedule supplies bounded +retry pressure. + +##### Problem + +A batch worker is writing a deterministic export object to storage. The network +can drop, the service can throttle, and the worker can lose the response after +the server has already accepted bytes. + +Blind retrying is risky. A second attempt might create a duplicate object, leave +an incomplete multipart upload behind, or put avoidable pressure on a shared +storage account. The retry policy must be bounded, and the upload operation must +be written so a repeated attempt has a well-defined outcome. + +##### When to use it + +Use this recipe when the upload target is idempotent by design: the object key +is deterministic, the content checksum is stable, conditional writes are used, +or the storage API supports an idempotency token or resumable upload id. + +It is a good fit for batch exports, reports, media processing outputs, data lake +ingestion, and checkpoint files where transient failures should recover without +requiring an operator to restart the whole job. + +##### When not to use it + +Do not apply this schedule to an upload that generates a new object key on every +attempt. That turns a transient failure into possible duplicate data. + +Do not retry validation failures, forbidden writes, missing buckets, unsupported +storage classes, checksum mismatches, or request bodies that cannot be replayed. +Those are not timing problems. + +Do not rely on retry alone for multipart uploads. Multipart protocols also need +cleanup or resume rules for abandoned parts, and retries must not complete two +different upload sessions for the same logical file. + +##### Schedule shape + +`Schedule.exponential("250 millis")` spaces repeated failures with a growing +delay. This reduces pressure on object storage when the service is already slow +or throttling. + +`Schedule.jittered` randomizes the selected delay so many workers do not retry +the same storage bucket at the same instant. + +`Schedule.recurs(5)` caps the number of retries after the original attempt. The +first upload still runs immediately; the schedule only controls follow-up +attempts after typed failures. + +`Schedule.during("30 seconds")` caps the elapsed retry window. Combining the +count limit and elapsed budget with `Schedule.both` means both limits must +allow another retry. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class UploadError extends Data.TaggedError("UploadError")<{ + readonly reason: + | "Timeout" + | "Throttled" + | "Unavailable" + | "ChecksumMismatch" + | "Forbidden" + | "BadRequest" +}> {} + +interface UploadRequest { + readonly bucket: string + readonly key: string + readonly body: Uint8Array + readonly checksumSha256: string + readonly idempotencyKey: string +} + +let attempts = 0 + +const uploadObject: (request: UploadRequest) => Effect.Effect = Effect.fnUntraced( + function*(request: UploadRequest) { + attempts += 1 + yield* Console.log(`upload attempt ${attempts}: ${request.bucket}/${request.key}`) + + if (attempts === 1) { + return yield* Effect.fail(new UploadError({ reason: "Throttled" })) + } + if (attempts === 2) { + return yield* Effect.fail(new UploadError({ reason: "Timeout" })) + } + + yield* Console.log(`stored checksum ${request.checksumSha256}`) + } +) + +const isTransientStorageError = (error: UploadError) => + error.reason === "Timeout" || + error.reason === "Throttled" || + error.reason === "Unavailable" + +const uploadRetryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(5)), + Schedule.both(Schedule.during("200 millis")) +) + +const uploadReport = (body: Uint8Array, checksumSha256: string) => + uploadObject({ + bucket: "reports", + key: `daily/${checksumSha256}.json`, + body, + checksumSha256, + idempotencyKey: checksumSha256 + }).pipe( + Effect.retry({ + schedule: uploadRetryPolicy, + while: isTransientStorageError + }) + ) + +const program = uploadReport( + new TextEncoder().encode("{\"rows\":3}"), + "sha256-demo" +).pipe( + Effect.flatMap(() => Console.log("upload complete")), + Effect.catch((error: UploadError) => Console.log(`upload failed: ${error.reason}`)) +) + +void Effect.runPromise(program) +// Output: +// upload attempt 1: reports/daily/sha256-demo.json +// upload attempt 2: reports/daily/sha256-demo.json +// upload attempt 3: reports/daily/sha256-demo.json +// stored checksum sha256-demo +// upload complete +``` + +The object key and idempotency key are derived from the content checksum. If the +worker retries after losing a response, it is still asking storage to create the +same logical object, not a new one. In a real client, pair this with the storage +provider's conditional write, checksum, or idempotency feature so a duplicate +attempt is recognized as the same upload. + +If the service returns `Timeout`, `Throttled`, or `Unavailable`, the failure is +fed to the schedule. If it returns `ChecksumMismatch`, `Forbidden`, or +`BadRequest`, retrying stops immediately and the typed failure is returned. + +##### Variants + +For small user-facing uploads, shorten both limits. For large batch uploads, +prefer a slower policy over a larger retry count. + +For multipart uploads, retry the smallest safe unit. Retrying an individual part +with a stable upload id, part number, and part checksum is usually safer than +restarting the whole object. Retrying completion is only safe when the complete +request is deterministic and the provider treats duplicate completion as +idempotent or already completed. + +##### Notes and caveats + +`Effect.retry` feeds typed failures into the schedule. The `while` predicate +classifies storage errors before the schedule spends another retry. + +`Schedule.during` bounds the retry window at recurrence decision points. It does +not cancel an upload attempt that is already in flight. If each attempt needs an +individual deadline, apply a timeout to `uploadObject` separately. + +Retry policy cannot replace storage-level correctness. Use deterministic object +names, checksums, conditional writes, idempotency keys, resumable upload ids, and +multipart cleanup rules so retrying a failed attempt is operationally safe. + +#### 29.4 Retry import processing after transient failures + +Import retry policies belong around one idempotent processing step. They should +show the retry cadence, the retry budget, and the failure classifier without +hiding permanent data problems. + +##### Problem + +An import worker is processing one batch from object storage into a staging +database and an enrichment service. Storage timeouts or temporary database +unavailability may clear on another attempt; invalid CSV structure or a +violated domain rule should stop immediately. + +The retry policy needs to be local to the idempotent processing step. Do not +wrap the whole worker loop in a retry if only the batch write or enrichment call +is transient. Retrying too much work can duplicate side effects, hide a bad +record, or keep an unhealthy dependency under pressure. + +##### When to use it + +Use this recipe when a single import batch can be safely attempted more than +once. That usually means the processor uses a stable import id, deduplicates +records, writes through upserts or transactions, and can resume without creating +duplicate rows or duplicate external events. + +It fits batch imports from files, queue-backed import jobs, and ETL staging +steps where operational failures are expected but should remain bounded. + +##### When not to use it + +Do not retry malformed input. A missing required column, invalid encoding, +failed schema validation, or rejected business rule will usually fail again +after the delay. + +Do not retry processing that is not idempotent. If a retry can insert the same +customer twice, send the same notification twice, or publish the same accounting +event twice, fix that boundary first with an idempotency key, unique constraint, +transactional write, or outbox. + +Do not use a retry schedule as a queue visibility timeout or leasing mechanism. +Let the queue or job coordinator own claiming and redelivery. Use `Schedule` for +the local decision to reattempt one failed effect. + +##### Schedule shape + +Use `Effect.retry` around the idempotent import step. With retry, the original +attempt runs immediately; the schedule controls only follow-up attempts after +typed failures. + +For transient import failures, a small exponential backoff is a better default +than a fixed interval because it backs away from overloaded storage, databases, +or enrichment services. `Schedule.exponential` keeps increasing the delay and +does not stop by itself, so combine it with `Schedule.recurs`. Add +`Schedule.jittered` when many workers may retry similar imports at the same +time. + +Keep retry eligibility in an error predicate. The schedule describes timing and +limits; the predicate decides whether the typed failure is transient enough to +retry. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +class StorageTimeout extends Data.TaggedError("StorageTimeout")<{ + readonly importId: string +}> {} + +class StagingDatabaseUnavailable extends Data.TaggedError("StagingDatabaseUnavailable")<{ + readonly importId: string +}> {} + +class InvalidImportFile extends Data.TaggedError("InvalidImportFile")<{ + readonly importId: string + readonly reason: string +}> {} + +class DuplicateExternalEventRisk extends Data.TaggedError("DuplicateExternalEventRisk")<{ + readonly importId: string +}> {} + +type ImportError = + | StorageTimeout + | StagingDatabaseUnavailable + | InvalidImportFile + | DuplicateExternalEventRisk + +interface ImportBatch { + readonly importId: string + readonly sourceUri: string +} + +const batch: ImportBatch = { + importId: "import-2026-05-17", + sourceUri: "s3://imports/customers.csv" +} + +let attempts = 0 + +const processImportBatch: (batch: ImportBatch) => Effect.Effect = Effect.fnUntraced( + function*(batch: ImportBatch) { + attempts += 1 + yield* Console.log(`import attempt ${attempts}: ${batch.sourceUri}`) + + if (attempts === 1) { + return yield* Effect.fail(new StorageTimeout({ importId: batch.importId })) + } + if (attempts === 2) { + return yield* Effect.fail(new StagingDatabaseUnavailable({ importId: batch.importId })) + } + + yield* Console.log(`imported batch ${batch.importId}`) + } +) + +const isTransientImportError = (error: ImportError): boolean => { + switch (error._tag) { + case "StorageTimeout": + case "StagingDatabaseUnavailable": + return true + case "InvalidImportFile": + case "DuplicateExternalEventRisk": + return false + } +} + +const retryTransientImportFailure = Schedule.exponential("10 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(5)) +) + +const program = processImportBatch(batch).pipe( + Effect.retry({ + schedule: retryTransientImportFailure, + while: isTransientImportError + }), + Effect.flatMap(() => Console.log("import finished")), + Effect.catch((error: ImportError) => Console.log(`import failed: ${error._tag}`)) +) + +void Effect.runPromise(program) +// Output: +// import attempt 1: s3://imports/customers.csv +// import attempt 2: s3://imports/customers.csv +// import attempt 3: s3://imports/customers.csv +// imported batch import-2026-05-17 +// import finished +``` + +`program` processes the batch once immediately. If object storage times out or +the staging database is temporarily unavailable, the retry policy uses jittered +exponential backoff for at most five retries after the original attempt. If the +file is invalid, or the processor detects that retrying could duplicate an +external event, retrying stops immediately and the typed error is propagated. + +The example assumes `processImportBatch` is idempotent for the retryable cases: +it uses `importId` as the stable identity for writes, can observe already +imported records, and does not emit irreversible side effects until the +transactional import state says it is safe. + +##### Variants + +For a short interactive import preview, keep the budget smaller. For a +background import worker, use a slower base delay and `Schedule.tapInput` to log +each retry input. + +For a dependency that exposes a precise `Retry-After` value, keep that timing +near the adapter and make the schedule responsible for the maximum number of +reattempts. Do not mix retry-after handling with validation or idempotency +checks. + +##### Notes and caveats + +`Effect.retry` feeds typed failures into the schedule. That is why +`Schedule.tapInput` can observe an `ImportError` in the background-worker +variant. `Effect.repeat` is the wrong tool for this recipe because it feeds +successful values into the schedule and stops on failure unless the failure is +handled separately. + +`Schedule.recurs(5)` means five retries after the original attempt, not five +total executions. Because `Schedule.exponential` is unbounded, keep an explicit +limit on import retries unless another operational budget is enforcing a +stricter bound. + +A retry policy cannot make an unsafe import safe. Make idempotency part of the +processor contract, and treat any uncertainty about duplicate side effects as a +non-retryable typed failure. + +#### 29.5 Pace reprocessing of failed records + +Failed-record reprocessing is a background repair path. It should make steady +progress without turning stale failures into constant database pressure. + +##### Problem + +A worker reads records marked failed, re-runs the operation for a small batch, +and updates each record as completed or still failed. Without a spaced +recurrence policy, it can fall into a tight scan/write loop against the same +rows. + +The recurrence policy needs to answer three operational questions: + +- how much time to leave between reprocessing passes +- how many follow-up passes the worker will run +- whether each pass is safe to repeat when the same record is seen again + +##### Schedule shape + +Model one reprocessing pass as an `Effect`, then repeat that pass with +`Schedule.spaced`. `Schedule.spaced("30 seconds")` waits for thirty seconds +after a pass completes before the next pass starts. That is usually the right +shape for database repair work because a slow pass naturally reduces the rate of +future database reads and writes. + +Limit the schedule with `Schedule.take` when the worker is invoked as a bounded +job. A daemon can use the same base cadence with a larger limit, a longer +interval, or an outer supervisor that starts the worker again. + +##### Example + +```ts runnable deterministic +import { Console, Data, Effect, Schedule } from "effect" + +type FailedRecord = { + readonly id: string + readonly payload: unknown +} + +class ReprocessError extends Data.TaggedError("ReprocessError")<{ + readonly recordId: string +}> {} + +let pass = 0 +const remainingAttempts = new Map([ + ["record-a", 1], + ["record-b", 2] +]) + +const loadFailedRecords = Effect.gen(function*() { + pass += 1 + const records = Array.from(remainingAttempts.keys()).map((id) => ({ + id, + payload: { id } + })) + yield* Console.log(`pass ${pass}: loaded ${records.length} failed records`) + return records +}) + +const reprocessRecord: (record: FailedRecord) => Effect.Effect = Effect.fnUntraced( + function*(record: FailedRecord) { + const attemptsLeft = remainingAttempts.get(record.id) ?? 0 + if (attemptsLeft > 1) { + remainingAttempts.set(record.id, attemptsLeft - 1) + return yield* Effect.fail(new ReprocessError({ recordId: record.id })) + } + remainingAttempts.delete(record.id) + yield* Console.log(`reprocessed ${record.id}`) + } +) + +const markRecordProcessed = (id: string) => Console.log(`marked ${id} processed`) + +const markRecordStillFailed = (id: string, _error: ReprocessError) => Console.log(`kept ${id} failed for another pass`) + +const reprocessFailedRecord = (record: FailedRecord) => + reprocessRecord(record).pipe( + Effect.andThen(markRecordProcessed(record.id)), + Effect.catchTag("ReprocessError", (error) => markRecordStillFailed(record.id, error)) + ) + +const reprocessFailedBatch = Effect.gen(function*() { + const records = yield* loadFailedRecords + + yield* Effect.forEach(records, reprocessFailedRecord, { + concurrency: 4 + }) +}) + +const reprocessingCadence = Schedule.spaced("10 millis").pipe( + Schedule.take(3) +) + +const program = Effect.repeat( + reprocessFailedBatch, + reprocessingCadence +).pipe( + Effect.flatMap(() => Console.log("reprocessing job finished")), + Effect.catch((error) => Console.log(`reprocessing failed: ${String(error)}`)) +) + +void Effect.runPromise(program) +// Output: +// pass 1: loaded 2 failed records +// reprocessed record-a +// marked record-a processed +// kept record-b failed for another pass +// pass 2: loaded 1 failed records +// reprocessed record-b +// marked record-b processed +// pass 3: loaded 0 failed records +// pass 4: loaded 0 failed records +// reprocessing job finished +``` + +##### Why spaced + +`Schedule.spaced` recurs continuously with the specified duration from the +previous run. In this recipe, the worker does not start the next database scan +until the previous batch has finished and the spacing delay has elapsed. + +That behavior is different from `Schedule.fixed`. A fixed schedule targets a +wall-clock interval. If a pass takes longer than the interval, the next pass may +start immediately. That is useful for heartbeats and sampling, but it is often +too aggressive for failed-record repair because slow database work is already a +signal to back off. + +##### Idempotency + +The record operation must be safe to run more than once. A failed-record table +can contain stale rows, workers can be restarted, and a previous pass can succeed +after writing only part of its bookkeeping. Use stable record identifiers, +idempotency keys, unique constraints, or compare-and-set updates so repeating +`reprocessRecord(record)` does not duplicate external writes. + +Keep classification outside the schedule. Permanent failures such as invalid +payloads should be marked as terminal or moved to a dead-letter workflow before +the scheduled repair loop sees them. The schedule controls timing; it should not +be the only thing preventing bad records from being retried forever. + +##### Database pressure + +The batch size, concurrency, and spacing are one policy. Increasing concurrency +without increasing the spacing can still overload the database because each pass +can issue more reads, writes, locks, and index updates. Start with a small batch +and a conservative concurrency value, then tune from observed queue depth and +database latency. + +For a fleet of workers, avoid making every instance wake at the same time. Once +the base cadence is correct, use `Schedule.jittered` to spread reprocessing +passes across the interval. Jitter reduces synchronized scans while preserving +the same reader-facing policy: failed records are retried gradually, not in a +burst. + +##### Notes and caveats + +`Effect.repeat` feeds successful values into the schedule. In this recipe the +batch effect handles individual record failures and succeeds after recording +their status, so the schedule controls the cadence of completed batch passes. + +If a whole batch fails because the database is unavailable, let that failure +escape and use a separate retry policy around the worker startup path. Mixing +record-level repair and infrastructure retry in one schedule makes the database +load profile harder to reason about. + +### 30. Product and Business Workflow Recipes + +#### 30.1 Poll payment settlement status + +Payment settlement is often asynchronous: the provider accepts the payment +first, then moves it through pending, processing, and terminal states. Model +those non-terminal states as successful observations and use a repeat schedule +to decide when another read is worth doing. + +##### Problem + +You need to poll a provider until settlement reaches a terminal state, but the +caller still needs a bounded answer. `Pending` and `Processing` are not errors; +they are successful responses that mean "poll again after a pause." + +##### When to use it + +Use this for checkout confirmation, payment reconciliation, and short-lived +API calls where the current request should wait briefly for settlement. The +status endpoint must be safe to call repeatedly. + +##### When not to use it + +Do not use this to hide provider failures. Authentication errors, invalid +payment ids, malformed requests, and transport failures belong in the error +channel or in a separate retry policy. Do not treat a timeout as a failed +payment; it only means this polling window ended before a terminal status was +seen. + +##### Schedule shape + +Use `Effect.repeat` because the decision is based on successful statuses. The +schedule keeps the latest status with `Schedule.passthrough`, continues while +the status is open, and also enforces a time budget. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type SettlementStatus = + | { readonly _tag: "Pending" } + | { readonly _tag: "Processing" } + | { readonly _tag: "Settled"; readonly settlementId: string } + | { readonly _tag: "Declined"; readonly reason: string } + +const statuses: ReadonlyArray = [ + { _tag: "Pending" }, + { _tag: "Processing" }, + { _tag: "Settled", settlementId: "set_123" } +] + +let reads = 0 + +const fetchSettlementStatus = Effect.gen(function*() { + const status = statuses[Math.min(reads, statuses.length - 1)] + reads += 1 + yield* Console.log(`provider status: ${status._tag}`) + return status +}) + +const isOpen = (status: SettlementStatus) => status._tag === "Pending" || status._tag === "Processing" + +const pollOpenSettlements = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => isOpen(input)), + Schedule.bothLeft( + Schedule.during("100 millis").pipe( + Schedule.satisfiesInputType() + ) + ) +) + +const program = fetchSettlementStatus.pipe( + Effect.repeat(pollOpenSettlements), + Effect.flatMap((status) => { + switch (status._tag) { + case "Settled": + return Console.log(`settled as ${status.settlementId}`) + case "Declined": + return Console.log(`declined: ${status.reason}`) + case "Pending": + case "Processing": + return Console.log(`timed out while ${status._tag}`) + } + }) +) + +Effect.runPromise(program) +// Output: +// provider status: Pending +// provider status: Processing +// provider status: Settled +// settled as set_123 +``` + +The first read happens immediately. The schedule controls only follow-up reads. +When the terminal `Settled` status appears, the repeat stops and the domain code +decides what to report. + +##### Variants + +Use a shorter budget for a checkout request and a slower cadence for background +reconciliation. Add `Schedule.jittered` when many payments may start polling at +the same time. + +##### Notes and caveats + +Keep settlement interpretation outside the schedule. The schedule answers +"should another status read happen?"; the payment workflow decides what +`Settled`, `Declined`, or a still-open timeout means. + +#### 30.2 Retry payment-status fetches + +Payment systems often separate the mutation that starts a payment from the read +that reports its state. Retrying a safe status read is different from retrying +the payment mutation itself. + +##### Problem + +You already have a payment id and need to fetch its latest status. The read can +fail because the provider times out, rate-limits briefly, or returns a transient +server error. Retry only those failures, with a bounded policy. + +##### When to use it + +Use this for safe reads such as `GET /payments/:id/status`. Repeating the read +must not create another charge, capture, refund, or ledger write. + +##### When not to use it + +Do not apply this policy to `POST /payments`, `POST /captures`, or +`POST /refunds` unless the provider contract gives you idempotency protection. +Do not retry permanent failures such as invalid credentials, malformed payment +ids, unsupported payment methods, or `404` responses for a payment that should +exist. + +##### Schedule shape + +Use `Effect.retry` because the schedule observes typed failures. Exponential +backoff controls pressure, `Schedule.jittered` avoids synchronized callers, +`Schedule.recurs` bounds retries, and `Schedule.while` filters retryable +failures. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type PaymentStatus = + | { readonly _tag: "Pending" } + | { readonly _tag: "Captured" } + +type PaymentStatusFetchError = { + readonly _tag: "PaymentStatusFetchError" + readonly status: number +} + +let attempts = 0 + +const fetchPaymentStatus = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`status fetch attempt ${attempts}`) + + if (attempts === 1) { + return yield* Effect.fail( + { + _tag: "PaymentStatusFetchError", + status: 503 + } as const + ) + } + if (attempts === 2) { + return yield* Effect.fail( + { + _tag: "PaymentStatusFetchError", + status: 429 + } as const + ) + } + + return { _tag: "Captured" } as const +}) + +const isRetryableStatusFetch = (error: PaymentStatusFetchError) => + error.status === 408 || error.status === 429 || error.status >= 500 + +const paymentStatusFetchRetry = Schedule.exponential("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.jittered, + Schedule.both(Schedule.recurs(5)), + Schedule.while(({ input }) => isRetryableStatusFetch(input)), + Schedule.tapInput((error) => Console.log(`retryable payment read failure: HTTP ${error.status}`)) +) + +const program = fetchPaymentStatus.pipe( + Effect.retry(paymentStatusFetchRetry), + Effect.flatMap((status) => Console.log(`final status: ${status._tag}`)) +) + +Effect.runPromise(program) +// Output: +// status fetch attempt 1 +// retryable payment read failure: HTTP 503 +// status fetch attempt 2 +// retryable payment read failure: HTTP 429 +// status fetch attempt 3 +// final status: Captured +``` + +The first read runs immediately. Only failures are fed to the retry schedule, +and only retryable status-fetch failures are allowed through the predicate. + +##### Variants + +Use a smaller budget for a user-facing request. Use a slower base delay for a +background reconciliation worker. Honor provider retry hints such as +`Retry-After` before falling back to local timing. + +##### Notes and caveats + +This recipe retries failed status fetches. It does not poll successful +`Pending` statuses until they become terminal; that is a repeat recipe over +successful values. + +#### 30.3 Poll order fulfillment progress + +Fulfillment moves through normal domain states: received, picking, packing, +shipped, delivered, canceled, or failed. Poll those states as successful data, +not as failures. + +##### Problem + +You need to show recent fulfillment progress without keeping a user request open +forever. The schedule should pause between reads, stop on terminal states, and +return the latest status when the budget ends. + +##### When to use it + +Use this for order pages, support tools, and checkout follow-up flows where a +short polling window is acceptable and a later push update or refresh can finish +the story. + +##### When not to use it + +Do not use this as a retry policy for a failing fulfillment endpoint. Add a +separate retry around the read if transport failures are expected. Do not turn +terminal business states into defects just to stop polling. + +##### Schedule shape + +Use `Schedule.spaced` for the read cadence, `Schedule.passthrough` to keep the +latest status, `Schedule.while` to continue only for non-terminal states, and +`Schedule.during` for the user-facing budget. + +##### Example + +```ts runnable +import { Console, Effect, Schedule } from "effect" + +type FulfillmentStatus = + | { readonly state: "received"; readonly orderId: string } + | { readonly state: "picking"; readonly orderId: string } + | { readonly state: "shipped"; readonly orderId: string } + | { readonly state: "delivered"; readonly orderId: string } + | { readonly state: "canceled"; readonly orderId: string } + +const statuses: ReadonlyArray = [ + { state: "received", orderId: "order-123" }, + { state: "picking", orderId: "order-123" }, + { state: "shipped", orderId: "order-123" }, + { state: "delivered", orderId: "order-123" } +] + +let reads = 0 + +const readFulfillmentStatus = Effect.sync(() => { + const status = statuses[Math.min(reads, statuses.length - 1)] + reads += 1 + console.log(`fulfillment status: ${status.state}`) + return status +}) + +const isTerminal = (status: FulfillmentStatus) => status.state === "delivered" || status.state === "canceled" + +const userFacingPolling = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => !isTerminal(input)), + Schedule.bothLeft( + Schedule.during("100 millis").pipe( + Schedule.satisfiesInputType() + ) + ) +) + +const program = readFulfillmentStatus.pipe( + Effect.repeat(userFacingPolling), + Effect.flatMap((status) => + isTerminal(status) + ? Console.log(`terminal fulfillment state: ${status.state}`) + : Console.log(`still in progress: ${status.state}`) + ) +) + +Effect.runPromise(program) +// Output may vary: elapsed timing can cross the user-facing polling budget boundary differently under load +// fulfillment status: received +// fulfillment status: picking +// fulfillment status: shipped +// fulfillment status: delivered +// terminal fulfillment state: delivered +``` + +The first status read is immediate. The schedule waits only before follow-up +reads and returns the final observed status. + +##### Variants + +Shorten the budget for checkout confirmation. Increase spacing for support +dashboards. Add jitter when many open views may poll together. + +##### Notes and caveats + +`Effect.repeat` feeds successful statuses into the schedule. Keep the mapping +from fulfillment status to UI behavior outside the schedule. + +#### 30.4 Retry notification delivery + +Notification delivery is externally visible. Retrying an email, SMS, webhook, +or push message is safe only when every attempt carries the same logical +identity and the receiver can deduplicate it. + +##### Problem + +You need to retry transient delivery failures without sending duplicates. The +retry policy should be bounded, spaced, and tied to a stable idempotency key. + +##### When to use it + +Use this for background notification workers and webhook dispatchers where the +provider accepts idempotency keys, message ids, or deduplication windows. + +##### When not to use it + +Do not retry malformed messages, invalid recipients, authorization failures, or +provider rejections that mean the notification will never be accepted. Do not +retry if the downstream system cannot tolerate duplicate delivery. + +##### Schedule shape + +Use `Effect.retry` because delivery failures drive recurrence. Use a short +exponential backoff, jitter for fleet safety, and a small retry count. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type Notification = { + readonly idempotencyKey: string + readonly recipient: string + readonly body: string +} + +type DeliveryError = + | { readonly _tag: "Timeout" } + | { readonly _tag: "ProviderUnavailable" } + +const notification: Notification = { + idempotencyKey: "notification-01HZYX8R7P0J9PAW4Q6V7N3QYB", + recipient: "user@example.com", + body: "Your export is ready." +} + +let attempts = 0 + +const sendWithIdempotency = (notification: Notification) => + Effect.gen(function*() { + attempts += 1 + yield* Console.log( + `send attempt ${attempts} with key ${notification.idempotencyKey}` + ) + + if (attempts < 3) { + return yield* Effect.fail({ _tag: "Timeout" } as const) + } + + yield* Console.log(`delivered to ${notification.recipient}`) + }) + +const retryTransientDelivery = Schedule.exponential("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.jittered, + Schedule.both(Schedule.recurs(5)), + Schedule.tapInput((error) => Console.log(`delivery retry after ${error._tag}`)) +) + +const program = sendWithIdempotency(notification).pipe( + Effect.retry(retryTransientDelivery) +) + +Effect.runPromise(program) +// Output: +// send attempt 1 with key notification-01HZYX8R7P0J9PAW4Q6V7N3QYB +// delivery retry after Timeout +// send attempt 2 with key notification-01HZYX8R7P0J9PAW4Q6V7N3QYB +// delivery retry after Timeout +// send attempt 3 with key notification-01HZYX8R7P0J9PAW4Q6V7N3QYB +// delivered to user@example.com +``` + +The first attempt happens immediately. The same `idempotencyKey` is used for +every retry, so duplicate suppression can happen outside the schedule. + +##### Variants + +Use fewer retries for a user-facing request. Use slower spacing for queue +workers under provider throttling. Keep jitter when many workers may retry the +same provider together. + +##### Notes and caveats + +`Schedule.recurs(5)` means at most five retries after the initial send attempt. +Generate the idempotency key for the logical notification, not for each attempt. + +#### 30.5 Repeat CRM sync every few minutes + +A CRM sync is a successful background workflow repeated over time. The schedule +should describe the cadence between completed sync passes; the sync itself +should handle idempotent writes and transient request retries internally. + +##### Problem + +You need to keep CRM data fresh by running a sync every few minutes, but a +hidden loop with sleeps makes spacing, overlap, and shutdown behavior hard to +review. + +##### When to use it + +Use `Effect.repeat` with `Schedule.spaced` when each sync pass should complete +before the quiet period begins. This fits cursor-based or updated-at-window CRM +integrations. + +##### When not to use it + +Do not use this as a retry policy for failed CRM requests. `Effect.repeat` +repeats successes and stops on failure. Keep transient retries inside the +single sync pass. Do not rely on scheduling to make writes idempotent. + +##### Schedule shape + +`Schedule.spaced("5 minutes")` waits after each successful sync before the next +run. The first sync starts immediately. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Schedule } from "effect" + +type SyncSummary = { + readonly cursor: string + readonly contactsUpserted: number + readonly companiesUpserted: number +} + +let pass = 0 + +const syncCrmOnce = Effect.gen(function*() { + pass += 1 + const summary = { + cursor: `cursor-${pass}`, + contactsUpserted: pass * 3, + companiesUpserted: pass + } + yield* Console.log( + `CRM sync ${pass}: ${summary.contactsUpserted} contacts, ` + + `${summary.companiesUpserted} companies` + ) + return summary +}) + +const demoCadence = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.take(2) +) + +const program = syncCrmOnce.pipe( + Effect.repeat(demoCadence), + Effect.flatMap((summary) => Console.log(`last cursor written: ${summary.cursor}`)) +) + +Effect.runPromise(program) +// Output: +// CRM sync 1: 3 contacts, 1 companies +// CRM sync 2: 6 contacts, 2 companies +// CRM sync 3: 9 contacts, 3 companies +// last cursor written: cursor-3 +``` + +The demo runs the first sync immediately and then two scheduled recurrences. In +production, use the real interval and tie the repeated fiber to service +lifetime. + +##### Variants + +Use `Schedule.fixed` only when wall-clock alignment matters more than a quiet +gap after completion. Add jitter when many instances run the same sync cadence. + +##### Notes and caveats + +Avoid overlap outside the local fiber too. If several processes can run the same +CRM sync, use a lease, partition ownership, advisory lock, queue assignment, or +another coordination mechanism. + +## Part VIII — Observability and Testing + +### 31. Observability, Logging, and Diagnostics + +#### 31.1 Log each retry attempt + +Retry logs should answer what failed, which policy handled it, and whether +another attempt was scheduled. Logging belongs at the boundary that owns the +retry policy. + +##### Problem + +You have a retried dependency call and want one clear log event for retry +behavior without duplicating final error reporting. + +##### When to use it + +Use this around HTTP requests, database calls, queue publishing, cache fills, +and startup probes where retry behavior matters during incident review. + +##### When not to use it + +Do not log large payloads, credentials, or full causes on every retry. Do not +use logging as a substitute for filtering permanent failures before retrying. + +##### Schedule shape + +Use `Schedule.tapInput` to observe the failure fed to `Effect.retry`. Use +`Schedule.tapOutput` to log only accepted retry steps. + +##### Example + +```ts runnable deterministic +import { Console, Duration, Effect, Schedule } from "effect" + +type RequestError = + | { readonly _tag: "RequestTimeout"; readonly endpoint: string } + | { readonly _tag: "ServiceUnavailable"; readonly endpoint: string } + +let attempts = 0 + +const fetchInventory = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`inventory attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail( + { + _tag: "RequestTimeout", + endpoint: "/inventory" + } as const + ) + } + + return ["sku-1", "sku-2"] +}) + +const retryInventoryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.both(Schedule.recurs(5)), + Schedule.tapInput((error) => Console.log(`retry input: ${error._tag} at ${error.endpoint}`)), + Schedule.tapOutput(([delay, retry]) => + Console.log( + `retry ${retry + 1} scheduled after ${Duration.format(delay)}` + ) + ) +) + +const program = fetchInventory.pipe( + Effect.retry(retryInventoryPolicy), + Effect.flatMap((items) => Console.log(`loaded ${items.length} items`)) +) + +Effect.runPromise(program) +// Output: +// inventory attempt 1 +// retry input: RequestTimeout at /inventory +// retry 1 scheduled after 10ms +// inventory attempt 2 +// retry input: RequestTimeout at /inventory +// retry 2 scheduled after 20ms +// inventory attempt 3 +// loaded 2 items +``` + +The input log records the typed failure. The output log runs only when the +schedule accepts another recurrence. + +##### Variants + +For hot paths, log only `tapOutput` so final non-retried failures are not logged +twice. Keep detailed error reporting at the final failure boundary. + +##### Notes and caveats + +`Schedule.recurs(5)` outputs a zero-based recurrence count, so the log prints +`retry + 1` for a human-facing retry number. + +#### 31.2 Log computed delays + +Backoff policies are easier to operate when the selected wait is visible. A log +line that only says "retrying" leaves operators guessing whether the next wait +is milliseconds or seconds. + +##### Problem + +You want retry logs to include the computed delay while keeping the policy +declarative. Do not duplicate the backoff formula in logging code. + +##### When to use it + +Use this for exponential, fibonacci, capped, or jittered policies where timing +explains caller latency and downstream pressure. + +##### When not to use it + +Do not log sensitive request or response data just because it is available near +the retry. Keep permanent-error classification separate from delay logging. + +##### Schedule shape + +For `Schedule.exponential`, the output is the base duration. Log it with +`Schedule.tapOutput`. If later combinators modify the actual delay, log close +to the combinator whose output you want to observe. + +##### Example + +```ts runnable deterministic +import { Console, Duration, Effect, Schedule } from "effect" + +type RetryError = { + readonly _tag: "Timeout" | "Unavailable" +} + +let attempts = 0 + +const callWebhook = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`webhook attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail({ _tag: "Timeout" } as const) + } +}) + +const retryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.tapOutput((delay) => Console.log(`base retry delay: ${Duration.format(delay)}`)), + Schedule.jittered, + Schedule.take(5) +) + +const program = callWebhook.pipe( + Effect.retry(retryPolicy), + Effect.flatMap(() => Console.log("webhook delivered")) +) + +Effect.runPromise(program) +// Output: +// webhook attempt 1 +// base retry delay: 10ms +// webhook attempt 2 +// base retry delay: 20ms +// webhook attempt 3 +// webhook delivered +``` + +The example logs the base exponential delay. `Schedule.jittered` changes the +sleep around that base delay, but the log still explains the shape of the +policy. + +##### Variants + +For a capped policy, log both the base delay and the capped delay at the point +where the cap is applied. For high-volume paths, export the delay as a metric +instead of logging every retry. + +##### Notes and caveats + +`Schedule.tapOutput` observes outputs and does not change them. With +`Effect.retry`, schedule inputs are failures; with `Effect.repeat`, inputs are +successful values. + +#### 31.3 Track total retry duration + +Retry count says how many follow-up attempts were scheduled. It does not say +how much time the caller spent inside the retry window. + +##### Problem + +You need logs or metrics that show total elapsed retry time, not only the next +delay. The elapsed value helps explain user latency and how much of the retry +budget has already been consumed. + +##### When to use it + +Use this for dependency calls, queue publication, webhook delivery, startup +checks, and background workers where retry latency is part of the service +contract. + +##### When not to use it + +Do not use `Schedule.elapsed` as the whole policy. It observes elapsed schedule +time; it does not provide spacing or a stopping condition by itself. + +##### Schedule shape + +Combine the real retry cadence with `Schedule.elapsed`. The cadence still owns +delays and limits; elapsed time is additional output for observability. + +##### Example + +```ts runnable +import { Console, Duration, Effect, Schedule } from "effect" + +type DependencyError = { + readonly _tag: "DependencyError" + readonly status: number +} + +let attempts = 0 + +const callDependency = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`dependency attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail( + { + _tag: "DependencyError", + status: 503 + } as const + ) + } + + return "ok" +}) + +const isRetryable = (error: DependencyError) => error.status === 408 || error.status === 429 || error.status >= 500 + +const retryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.both(Schedule.recurs(5)), + Schedule.bothWith( + Schedule.elapsed, + ([nextDelay, retryIndex], elapsed) => ({ + elapsed, + nextDelay, + retryIndex + }) + ), + Schedule.tapOutput(({ elapsed, nextDelay, retryIndex }) => + Console.log( + `retry=${retryIndex + 1} elapsed=${Duration.toMillis(elapsed)}ms ` + + `next=${Duration.toMillis(nextDelay)}ms` + ) + ) +) + +const program = callDependency.pipe( + Effect.retry({ + schedule: retryPolicy, + while: isRetryable + }), + Effect.flatMap((value) => Console.log(`dependency result: ${value}`)) +) + +Effect.runPromise(program) +// Output may vary: measured elapsed time and selected delays depend on runtime timing +// dependency attempt 1 +// retry=1 elapsed=0ms next=10ms +// dependency attempt 2 +// retry=2 elapsed=11ms next=20ms +// dependency attempt 3 +// dependency result: ok +``` + +The next delay explains immediate pressure on the dependency. The elapsed value +explains how long the retry window has been active. + +##### Variants + +For user-facing paths, keep the elapsed budget small and the retry count low. +For background work, use a slower base delay and a wider budget, but still log +elapsed retry time separately from the final operation outcome. + +##### Notes and caveats + +`Schedule.during` and `Schedule.elapsed` are about recurrence windows. They do +not interrupt an already-running attempt; add an Effect timeout around the +operation when each attempt needs a hard deadline. + +#### 31.4 Surface termination reasons + +Schedules decide whether another recurrence is allowed. They do not invent +business meaning for the final value or failure. Put that interpretation in the +Effect code around the schedule. + +##### Problem + +Callers and operators need to distinguish success, terminal domain failure, +timeout, fatal read failure, and exhausted retry budget. A schedule gives you +mechanics; your workflow should surface the reason. + +##### When to use it + +Use this for job polling, provisioning workflows, dependency probes, and remote +API retries where "completed", "failed", "timed out", and "gave up" are +different outcomes. + +##### When not to use it + +Do not ask `Schedule.during` to throw a timeout error. It simply stops allowing +future recurrences. Do not classify fatal errors as retryable just so the +schedule can see them. + +##### Schedule shape + +For polling, keep the latest status as output and stop when the status is no +longer running or the budget is exhausted. Then inspect the final value. + +##### Example + +```ts runnable +import { Console, Effect, Schedule } from "effect" + +type JobStatus = + | { readonly _tag: "Running"; readonly jobId: string } + | { readonly _tag: "Done"; readonly jobId: string; readonly resultId: string } + | { readonly _tag: "Failed"; readonly jobId: string; readonly reason: string } + +type PollTermination = + | { readonly _tag: "Completed" } + | { readonly _tag: "TerminalFailure"; readonly reason: string } + | { readonly _tag: "TimedOut"; readonly lastStatus: "Running" } + +let reads = 0 + +const checkJobStatus = Effect.sync((): JobStatus => { + reads += 1 + const status: JobStatus = reads < 4 + ? { _tag: "Running", jobId: "job-1" } + : { _tag: "Done", jobId: "job-1", resultId: "result-1" } + console.log(`job status: ${status._tag}`) + return status +}) + +const pollUntilTerminalOrBudget = Schedule.spaced("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input._tag === "Running"), + Schedule.bothLeft( + Schedule.during("25 millis").pipe( + Schedule.satisfiesInputType() + ) + ) +) + +const toTermination = (status: JobStatus): PollTermination => { + switch (status._tag) { + case "Done": + return { _tag: "Completed" } + case "Failed": + return { _tag: "TerminalFailure", reason: status.reason } + case "Running": + return { _tag: "TimedOut", lastStatus: "Running" } + } +} + +const program = checkJobStatus.pipe( + Effect.repeat(pollUntilTerminalOrBudget), + Effect.flatMap((status) => Console.log(`termination reason: ${toTermination(status)._tag}`)) +) + +Effect.runPromise(program) +// Output may vary: elapsed timing can cross the polling budget boundary differently under load +// job status: Running +// job status: Running +// job status: Running +// job status: Done +// termination reason: Completed +``` + +The timeout reason comes from interpreting the final `Running` status. It is +not produced directly by `Schedule.during`. + +##### Variants + +For retry workflows, interpret the final failure from `Effect.retry`: a final +transient error can mean the retry budget was exhausted, while a fatal error +means the retry predicate stopped recurrence immediately. + +##### Notes and caveats + +Keep retryability or terminal-state information in typed domain data. That +makes the final reason explicit instead of hiding it inside timing policy. + +#### 31.5 Measure schedule effectiveness + +A retry or polling schedule is useful only when it improves outcomes more than +it increases latency and load. Measure both sides. + +##### Problem + +You have a retry policy that looks reasonable, but you need evidence that it is +helping. Count scheduled recurrences, record chosen delays, and measure final +outcomes outside the schedule. + +Beginner note: Schedule output — schedule metrics explain the recurrence policy. +Keep business success and failure metrics around the effect that uses the +policy. + +##### When to use it + +Use this when retry or polling affects user latency, infrastructure load, +downstream quotas, incident diagnosis, or operational cost. + +##### When not to use it + +Do not measure retries as a substitute for classifying errors. Validation, +authorization, malformed requests, and unsafe non-idempotent writes should be +excluded before the schedule is applied. + +##### Schedule shape + +Use `Schedule.tapInput` for recurrence inputs and `Schedule.tapOutput` for +schedule outputs. Keep operation-level success and failure metrics around the +effect that uses the policy. + +##### Example + +```ts runnable deterministic +import { Console, Duration, Effect, Metric, Schedule } from "effect" + +type InventoryError = { + readonly _tag: "Timeout" | "Unavailable" | "BadRequest" +} + +let attempts = 0 + +const fetchInventory: Effect.Effect, InventoryError> = Effect.gen(function*() { + attempts += 1 + yield* Console.log(`inventory attempt ${attempts}`) + + if (attempts < 3) { + return yield* Effect.fail({ _tag: "Unavailable" } as const) + } + + return ["sku-1", "sku-2", "sku-3"] +}) + +const retryScheduled = Metric.counter("inventory_retry_scheduled_total", { + description: "Retries scheduled by the inventory retry policy" +}) + +const retryDelayMillis = Metric.histogram("inventory_retry_delay_millis", { + description: "Base retry delay before jitter", + boundaries: [10, 20, 50, 100] +}) + +const inventoryRetryPolicy = Schedule.exponential("10 millis").pipe( + Schedule.satisfiesInputType(), + Schedule.tapOutput((delay) => + Effect.gen(function*() { + yield* Metric.update(retryDelayMillis, Duration.toMillis(delay)) + yield* Console.log(`observed retry delay ${Duration.toMillis(delay)}ms`) + }) + ), + Schedule.jittered, + Schedule.take(5), + Schedule.tapInput((error) => + Effect.gen(function*() { + yield* Metric.update(retryScheduled, 1) + yield* Console.log(`scheduled retry after ${error._tag}`) + }) + ) +) + +const program = fetchInventory.pipe( + Effect.retry({ + schedule: inventoryRetryPolicy, + while: (error) => error._tag !== "BadRequest" + }), + Effect.flatMap((items) => Console.log(`inventory loaded after retry: ${items.length} items`)) +) + +Effect.runPromise(program) +// Output: +// inventory attempt 1 +// scheduled retry after Unavailable +// observed retry delay 10ms +// inventory attempt 2 +// scheduled retry after Unavailable +// observed retry delay 20ms +// inventory attempt 3 +// inventory loaded after retry: 3 items +``` + +The counter records scheduled retries, not the initial attempt. The histogram +records the base delay before jitter. Final success is measured around the +operation, outside the schedule. + +##### Variants + +For polling, count scheduled polls, terminal success, terminal timeout, and +elapsed time until the desired state appears. For fleet-wide retries, compare +retry counters with downstream saturation signals such as 429s, 503s, queue +depth, and connection pool usage. + +##### Notes and caveats + +Measure benefit and cost together. A policy that increases eventual success +while hiding an outage or adding too much latency is not effective. + +### 32. Testing Recipes + +#### 32.1 Assert retry count + +Retry-count tests should count effect evaluations. They should not infer retry +count from elapsed time or from the schedule output. + +Beginner note: Recurrence counts — assert the number of effect attempts when you +care about retry count; schedule outputs can be transformed or combined. + +##### Problem + +`Schedule.recurs(3)` is often misread as "three total attempts". With +`Effect.retry`, the original attempt runs first. The schedule is consulted only +after a typed failure, so three recurrences means three retries after that +original attempt. + +##### When to use it + +Use this shape when the contract is the retry budget: + +- a permanently failing fixture should be evaluated `1 + retries` times +- a transient fixture should stop as soon as it succeeds +- a count-based policy should be tested without relying on real time + +##### When not to use it + +Do not use a count test to prove delay behavior. Delay tests need clock control. +Also keep jitter out of this test; random delay changes make the timing +contract harder to see and do not affect the retry count. + +##### Schedule shape + +Use `Schedule.recurs(n)` for a pure retry-count limit. Its output is the +zero-based recurrence count, but for this test the important value is the number +of times the effect itself was evaluated. + +##### Example + +```ts runnable deterministic +import { Console, Effect, Exit, Ref, Schedule } from "effect" + +type TestError = { readonly _tag: "TestError" } +const testError: TestError = { _tag: "TestError" } + +const alwaysFails = Effect.fnUntraced(function*(attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`attempt ${attempt}`) + return yield* Effect.fail(testError) +}) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + + const exit = yield* alwaysFails(attempts).pipe( + Effect.retry(Schedule.recurs(3)), + Effect.exit + ) + + const totalAttempts = yield* Ref.get(attempts) + yield* Console.log(`total attempts: ${totalAttempts}`) + yield* Console.log(`failed: ${Exit.isFailure(exit)}`) +}) + +Effect.runPromise(program) +// Output: +// attempt 1 +// attempt 2 +// attempt 3 +// attempt 4 +// total attempts: 4 +// failed: true +``` + +##### Variants + +To prove early success, make the fixture fail while the counter is below a +threshold and succeed afterward. With `Schedule.recurs(3)`, a fixture that +succeeds on the third evaluation should leave the counter at `3`, because +`Effect.retry` stops as soon as the effect succeeds. + +If the production policy also has spacing or backoff, keep the count assertion +focused on evaluations. The timing policy can be tested separately with +`TestClock`. + +##### Notes and caveats + +`Effect.retry` feeds failures into the schedule. `Effect.repeat` feeds +successful values into the schedule. The same `Schedule.recurs(3)` value has +different operational meaning in those two contexts because retry recurrences +follow failures, while repeat recurrences follow successes. + +#### 32.2 Assert delays between retries + +Retry timing is observable behavior. Use `TestClock` to move virtual time +instead of making a test wait on the machine clock. + +##### Problem + +Counting attempts proves the retry limit, but it does not prove that retries +waited. For a policy with `Schedule.spaced("100 millis")`, check both sides of +the boundary: no retry at 99 milliseconds, then one retry after the remaining +millisecond. + +##### When to use it + +Use this recipe when immediate retry would change the contract or increase load: +HTTP retries, reconnect loops, startup dependency checks, and background worker +retries are common examples. + +##### When not to use it + +Do not use a timing test to decide whether an error should be retried. Classify +validation failures, authorization failures, malformed requests, and unsafe +non-idempotent writes before applying the retry policy. + +Do not assert exact timestamps for `Schedule.jittered`; jitter intentionally +changes each delay. Assert bounds or test the deterministic policy before jitter +is added. + +##### Schedule shape + +Combine a deterministic delay with a retry limit. With +`Schedule.spaced("100 millis").pipe(Schedule.both(Schedule.recurs(2)))`, the +original attempt runs immediately. Each failed attempt schedules the next retry +100 milliseconds later, up to two retries. + +##### Example + +```ts +import { Console, Effect, Fiber, Ref, Schedule } from "effect" +import { TestClock } from "effect/testing" + +const retryPolicy = Schedule.spaced("100 millis").pipe( + Schedule.both(Schedule.recurs(2)) +) + +const operation = Effect.fnUntraced(function*(attempts: Ref.Ref) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`attempt ${attempt}`) + + if (attempt < 3) { + return yield* Effect.fail("transient" as const) + } + + return "ok" as const +}) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const fiber = yield* operation(attempts).pipe( + Effect.retry(retryPolicy), + Effect.forkScoped + ) + + yield* Effect.yieldNow + const afterStart = yield* Ref.get(attempts) + yield* Console.log(`after start: ${afterStart}`) + + yield* TestClock.adjust("99 millis") + const beforeDelay = yield* Ref.get(attempts) + yield* Console.log(`after 99ms: ${beforeDelay}`) + + yield* TestClock.adjust("1 millis") + const afterFirstDelay = yield* Ref.get(attempts) + yield* Console.log(`after 100ms: ${afterFirstDelay}`) + + yield* TestClock.adjust("100 millis") + const result = yield* Fiber.join(fiber) + const finalAttempts = yield* Ref.get(attempts) + + yield* Console.log(`result: ${result}`) + yield* Console.log(`total attempts: ${finalAttempts}`) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program) +``` + +The retrying operation runs in a fiber because it sleeps after each failure. +Advancing by 99 milliseconds shows that no retry has started early. Advancing by +the remaining millisecond releases the first sleep. The final adjustment +releases the second retry, which succeeds. + +##### Notes and caveats + +`Effect.retry` feeds failures into the schedule. It returns the successful value +from the retried effect, or the last failure if the retry policy is exhausted. +The schedule output is useful for composition and observation, but it is not the +result returned by the retrying operation. + +`Schedule.spaced` contributes a constant delay between recurrence decisions. +`Schedule.recurs(n)` bounds the number of recurrences, so with retry it permits +`n` retries after the original attempt. + +Use `Schedule.delays` when you want to test the delay sequence as schedule data. +Use `TestClock.adjust` when the test runs a real retry loop. + +#### 32.3 Simulate transient failures + +Transient-failure tests should use a deterministic fixture. Random failure, a +live dependency, or wall-clock waiting makes the retry behavior hard to inspect. + +##### Problem + +Model the dependency as an effect whose first few evaluations fail and whose +later evaluations may succeed. The tests should cover both sides of the retry +budget: recovery when the failures fit within the schedule, and final failure +when they outlast it. + +##### Schedule shape + +Use a small deterministic policy such as +`Schedule.spaced("100 millis").pipe(Schedule.both(Schedule.recurs(3)))`. +`Schedule.spaced` adds the delay before each retry. `Schedule.recurs(3)` allows +three retries after the initial attempt. If the effect fails four times in a +row, `Effect.retry` returns the fourth failure. + +##### Example + +```ts +import { Console, Data, Effect, Fiber, Ref, Schedule } from "effect" +import { TestClock } from "effect/testing" + +class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{ + readonly attempt: number +}> {} + +const retryPolicy = Schedule.spaced("100 millis").pipe( + Schedule.both(Schedule.recurs(3)) +) + +const flakyRequest = Effect.fnUntraced(function*( + failuresBeforeSuccess: number, + attempts: Ref.Ref +) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`attempt ${attempt}`) + + if (attempt <= failuresBeforeSuccess) { + return yield* Effect.fail(new ServiceUnavailable({ attempt })) + } + + return "ok" as const +}) + +const successfulCase = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const fiber = yield* flakyRequest(2, attempts).pipe( + Effect.retry(retryPolicy), + Effect.forkScoped + ) + + yield* TestClock.adjust("100 millis") + yield* TestClock.adjust("100 millis") + + const result = yield* Fiber.join(fiber) + const count = yield* Ref.get(attempts) + yield* Console.log(`success case: ${result} after ${count} attempts`) +}) + +const exhaustedCase = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const fiber = yield* flakyRequest(4, attempts).pipe( + Effect.retry(retryPolicy), + Effect.flip, + Effect.forkScoped + ) + + yield* TestClock.adjust("100 millis") + yield* TestClock.adjust("100 millis") + yield* TestClock.adjust("100 millis") + + const error = yield* Fiber.join(fiber) + const count = yield* Ref.get(attempts) + yield* Console.log( + `exhausted case: ${error._tag}(${error.attempt}) after ${count} attempts` + ) +}) + +const program = Effect.gen(function*() { + yield* Console.log("recovers within budget") + yield* successfulCase + yield* Console.log("outlasts retry budget") + yield* exhaustedCase +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program) +``` + +##### Why this works + +The fixture stores its attempt count in a `Ref`, so each call observes and +updates state inside `Effect`. The first run fails twice and succeeds on the +third evaluation. The second run fails four times; the policy allows only three +retries after the initial attempt, so the fourth failure is returned. + +##### Notes and caveats + +Use `TestClock.adjust` for retry delays in tests. Do not make schedule tests +sleep on wall-clock time. Keep jitter out of this fixture; add a separate test +for jitter bounds if production uses `Schedule.jittered`. + +#### 32.4 Verify no retry on fatal errors + +Retry tests should prove classification as well as timing. A schedule may allow +several recurrences, but a fatal domain error should bypass the retry loop. + +##### Problem + +The operation exposes one typed error channel with both transient and fatal +cases. Run a fatal fixture under a policy that would retry transient failures, +then check that the fatal error is returned after one evaluation. + +##### When to use it + +Use this test when the retry boundary receives classified domain errors such as +`RateLimited`, `Timeout`, `InvalidCredentials`, or `MalformedRequest`. + +##### When not to use it + +Do not use a schedule predicate as the first place where errors are understood. +Classify errors near the effect that creates them, then let the schedule decide +recurrence for the retryable subset. Defects and interruptions are not typed +failures, so `Effect.retry` does not feed them into the retry schedule. + +##### Schedule shape + +Use a schedule that would clearly retry if classification allowed it, then add a +classification predicate to the retry options. + +```ts runnable deterministic +import { Console, Data, Effect, Ref, Schedule } from "effect" + +class TransientError extends Data.TaggedError("TransientError")<{ + readonly message: string +}> {} + +class FatalError extends Data.TaggedError("FatalError")<{ + readonly message: string +}> {} + +type ServiceError = TransientError | FatalError + +const isTransient = (error: ServiceError): error is TransientError => error._tag === "TransientError" + +const request = Effect.fnUntraced(function*( + attempts: Ref.Ref, + error: ServiceError +) { + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + yield* Console.log(`attempt ${attempt}: ${error._tag}`) + return yield* Effect.fail(error) +}) + +const program = Effect.gen(function*() { + const attempts = yield* Ref.make(0) + + const error = yield* request( + attempts, + new FatalError({ message: "invalid credentials" }) + ).pipe( + Effect.retry({ + schedule: Schedule.recurs(3), + while: isTransient + }), + Effect.flip + ) + + const count = yield* Ref.get(attempts) + yield* Console.log(`returned: ${error._tag}`) + yield* Console.log(`total attempts: ${count}`) +}) + +Effect.runPromise(program) +// Output: +// attempt 1: FatalError +// returned: FatalError +// total attempts: 1 +``` + +##### Why this catches regressions + +`Schedule.recurs(3)` would permit up to three retry recurrences after the first +failure. The `while` predicate receives each typed failure before another +attempt is made. Because `FatalError` is not transient, the retry policy stops +immediately. + +##### Variants + +Use `until` when the predicate reads more naturally as a stop condition, for +example `until: (error) => error._tag === "FatalError"`. Use `Schedule.spaced`, +`Schedule.exponential`, or a production retry policy in the test when you need +to verify that the same classification wraps the real schedule. + +##### Notes and caveats + +This recipe is about typed domain errors. If an effect dies with a defect or is +interrupted, `Effect.retry` does not feed that cause into the retry schedule. +For typed failures, the schedule input is the failure value, so predicates such +as `while` and `until` can inspect the classified error before the next +recurrence. + +#### 32.5 Test capped backoff behavior + +Capped backoff tests should prove the delay sequence without depending on the +machine clock. + +##### Problem + +Given a retry policy that starts with exponential backoff and then stops growing +at a maximum delay, the test should show three facts: + +- early retries use the exponential curve +- later retries never wait longer than the cap +- the retry limit still counts retries after the original attempt + +A real-time sleep is slow and flaky. Exact assertions after `Schedule.jittered` +are also wrong because jitter intentionally changes each delay. + +##### When to use it + +Use this recipe when a retry policy has a hard maximum delay and you want a +fast test for the timing contract. It is a good fit for client libraries, +background workers, polling loops, and reconcilers where the cap is part of the +operational guarantee. + +##### When not to use it + +Do not test capped backoff by waiting for real milliseconds to pass. That makes +the test depend on scheduler load and wall-clock timing. + +Do not assert exact delays for a policy after `Schedule.jittered` has been +applied. `Schedule.jittered` randomly adjusts each recurrence delay between 80% +and 120% of the original delay, so exact timestamps are not the contract. + +##### Schedule shape + +Build the cap with `Schedule.modifyDelay`. `Schedule.exponential` computes each +backoff duration, and `modifyDelay` replaces the next recurrence delay with the +minimum of that duration and the cap. For a base of 100 milliseconds and a cap +of 250 milliseconds, the first five delays are 100, 200, 250, 250, and 250 +milliseconds. + +##### Example + +```ts +import { Console, Duration, Effect, Fiber, Schedule } from "effect" +import { TestClock } from "effect/testing" + +const cappedBackoff = Schedule.exponential("100 millis").pipe( + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.millis(250)))), + Schedule.delays, + Schedule.tapOutput((delay) => Console.log(`scheduled delay: ${Duration.toMillis(delay)}ms`)), + Schedule.take(5) +) + +const program = Effect.gen(function*() { + const fiber = yield* Effect.void.pipe( + Effect.repeat(cappedBackoff), + Effect.forkScoped + ) + + yield* Effect.yieldNow + yield* TestClock.adjust("2 seconds") + yield* Fiber.join(fiber) +}).pipe(Effect.provide(TestClock.layer()), Effect.scoped) + +Effect.runPromise(program) +``` + +The program repeats a no-op effect under the schedule, logs the computed delays, +and uses `TestClock` so the two seconds of virtual time pass immediately. + +##### Variants + +If the production policy is jittered, keep the cap test deterministic and keep +the hard cap after jitter. Test the jittered policy by seeding randomness or by +asserting bounds. Do not combine "capped" and "exact jittered delay" in the same +assertion. + +##### Notes and caveats + +`Schedule.exponential(base, factor)` computes delays as `base * factor ** n`, +where `n` is the number of recurrences so far. Its output is the current +duration, and the recurrence delay is the same duration. + +`Schedule.modifyDelay` changes the delay used before the next recurrence. It +does not change the schedule output. Use `Schedule.delays` when the test should +observe the actual delay after modifiers have been applied. + +`Schedule.recurs(n)` allows at most `n` retries when used with `Effect.retry`. +Those retries happen after the original attempt; they are not counted as part of +the first evaluation. + +## Part IX — Anti-Patterns + +### 33. Retrying Everything + +#### 33.1 Retry on validation errors + +Retrying validation errors is an anti-pattern because waiting does not change +an invalid request. A missing field, unsupported enum, malformed payload, failed +business rule, or rejected tenant boundary should be returned to the caller. + +##### The anti-pattern + +The problematic shape is a shared retry policy around an operation whose typed +errors have not been separated. The policy might use exponential backoff, a +retry count, or a time budget, but it runs before the validation failure is +classified as terminal. + +That sends validation failures through the same schedule as timeouts, temporary +unavailability, and rate limits. The invalid request is submitted again, delayed +again, logged again, and usually reported later than it should be. + +##### Why it happens + +It usually happens when retry is added before the error model is settled. A +schedule such as `Schedule.exponential("100 millis")` is easy to reuse, and a +backoff curve can make the retry look careful. But `Schedule.exponential` +describes timing; it does not know whether the failure is retryable, and it is +unbounded unless composed with a limit such as `Schedule.recurs`, +`Schedule.take`, or `Schedule.during`. + +The other common cause is placing retry too far outside the failing operation. +If a whole workflow is retried, a validation failure from one step can cause +unrelated steps to run again. + +##### Why it is risky + +Validation failures should be fast, stable, and actionable. Retrying them turns +a deterministic rejection into delayed operational noise. + +A single bad payload can appear as several failing attempts, while the real +issue is still one permanent input problem. If the retried operation contains +writes or external calls, the retry can also duplicate side effects unless the +operation is idempotent, meaning repeated attempts represent the same logical +operation. + +Jitter does not fix this. `Schedule.jittered` spreads delay around a schedule's +selected timing; it does not make an invalid request valid or decide whether a +failure belongs in the retry path. + +##### A better approach + +Classify before retrying. Keep the decision close to the domain boundary that +understands the failure: + +- validation, malformed request, authentication, authorization, tenant, and + business-rule failures should bypass retry and return immediately +- timeouts, connection resets, temporary unavailability, selected rate-limit + responses, and other explicitly transient failures may enter the retry policy +- unsafe writes need an idempotency or deduplication story before retry is + considered + +After classification, let the schedule do schedule work. Use exponential or +fixed spacing for the delay shape. Add `Schedule.recurs`, `Schedule.take`, or +`Schedule.during` so termination is visible. Add `Schedule.jittered` when many +callers may retry at the same time. Name the policy after the retryable case, +not after a generic operator. + +Use `Effect.retry`'s retry predicate at the boundary when the question is +"should this typed failure be retried?" Reserve `Schedule.while` for cases where +the schedule itself must stop based on schedule metadata such as input, output, +attempt, elapsed time, or selected delay. + +##### Notes and caveats + +A stricter policy may make failures visible sooner. That is the point. The caller +can distinguish "this request is invalid" from "this retryable operation +exhausted its budget", and operations can tell whether the retry policy is +protecting the system or merely delaying a permanent error. + +There are rare validation-like failures that are actually consistency problems, +such as a just-created reference not yet visible in another service. Model those +as transient consistency failures, not generic validation errors, and give them a +small, bounded retry policy that documents the assumption. + +#### 33.2 Retry on authorization failures + +Retrying authorization failures is an anti-pattern because time does not usually +change whether the caller is allowed to perform the operation. + +##### The anti-pattern + +The problematic version treats `401`, `403`, and other authorization errors as +transient transport failures. A broad retry policy is attached to an HTTP +client, repository, or service boundary, and every failure shape flows through +the same schedule. + +The schedule may be bounded and well tuned, but it is attached to the wrong +condition. `Schedule` describes when recurrence may continue; it does not know +that an expired session, missing scope, revoked key, disabled account, or tenant +mismatch needs a different response from a dropped connection. + +##### Why it happens + +It usually happens when retry is installed before the error model is classified. +Teams create one convenient "network retry" schedule and apply it around calls +that can fail for authentication, authorization, validation, rate limiting, and +infrastructure reasons. The schedule becomes a broad loop instead of a small +operational promise. + +Authorization failures are tempting to retry because some are recoverable. An +access token might have expired, a token refresh call might race with another +caller, or a permission cache might be stale. Those are narrow recovery flows. +Model them as credential refresh or authorization-state reload, not as retries +of the protected operation. + +##### Why it is risky + +Retried authorization failures create noisy security signals. They can look like +credential stuffing, abusive clients, or a broken integration repeatedly hitting +an endpoint with known-bad credentials. Backoff and jitter can reduce +synchronization, but they do not make an unauthorized request safer or more +correct. + +They also delay the next useful action. A user may need to sign in again, an +operator may need to grant a scope, a service may need a rotated secret, or the +caller may need a clear forbidden result. Retrying the denied operation makes +that feedback arrive later and can bury the original authorization reason under +an exhausted retry budget. + +##### A better approach + +Classify authorization failures before scheduling retries. Treat "not +authenticated" and "not authorized" as terminal for the protected operation. +Return or fail with the authorization error directly so the caller can redirect, +request permission, rotate credentials, or stop the workflow. + +If the suspected cause is an expired credential, isolate that behavior into a +token refresh flow. The refresh call may have its own small schedule, commonly +bounded with `Schedule.recurs` or `Schedule.take`, and it should retry only +failures that are transient for the refresh endpoint. After a successful +refresh, run the original operation once with the new credentials. If it is +still unauthorized, stop. + +Name the schedule after the recovery action, such as "refresh token briefly", +rather than "retry auth failures". The retry is for acquiring valid credentials, +not for repeatedly attempting a forbidden action. + +##### Notes and caveats + +Some systems have authorization state that is eventually consistent after a +grant or policy update. Even there, avoid a blanket retry on every `401` or +`403`. Use a narrow, bounded wait around the operation that observes +propagation, and make the reason visible in logs and metrics. + +Retry the thing that can become true with time. Network availability, refresh +endpoint availability, and policy propagation may qualify. A request made with +invalid, revoked, missing, or insufficient credentials does not. + +#### 33.3 Retry on malformed requests + +Retrying malformed requests is an anti-pattern because the request is already +structurally wrong. A bad JSON body, invalid content type, missing envelope, +corrupted signature base string, impossible query shape, or unparseable protocol +message will not become well formed because the caller waited. + +##### The anti-pattern + +The problematic shape treats request parsing and transport recovery as the same +failure path. A client, worker, or gateway wraps the whole operation in a shared +retry policy, so malformed input is submitted repeatedly under the same schedule +used for dropped connections, timeouts, or temporary service unavailability. + +The policy may look operationally responsible. It might use +`Schedule.exponential` for backoff, `Schedule.recurs` or `Schedule.take` for a +retry cap, `Schedule.during` for an elapsed budget, and `Schedule.jittered` to +avoid synchronized retries. Those are useful timing tools, but they do not +change the failure classification. A malformed request is still malformed on +every recurrence. + +##### Why it happens + +It usually happens when retry is installed at a boundary that cannot distinguish +wire, protocol, and domain failures. The schedule is added around an HTTP call, +message handler, or RPC operation before the error model separates +"temporary infrastructure problem" from "the caller sent something this endpoint +cannot interpret." + +Malformed requests are also easy to mislabel as transient because they may come +from integration drift. A caller may be on the wrong schema version, a producer +may serialize a field incorrectly, or a proxy may strip a required header. Those +problems are real, but the next retry of the same request carries the same +defect. The fix is to classify the failure and repair the producer, adapter, or +contract. + +##### Why it is risky + +Retried malformed requests hide the strongest signal you have: the request shape +is invalid at the boundary. Instead of a fast, stable rejection that points to a +contract problem, the system produces delayed failures, repeated logs, inflated +error counts, and unnecessary load on parsers, gateways, queues, and downstream +services. + +The retry can also make incident response worse. A burst of malformed messages +may look like a capacity or availability problem because the retry layer +multiplies the number of attempts. Backoff reduces the rate, and jitter spreads +the attempts out, but known-bad input still occupies the system. + +For message-driven systems, retrying malformed payloads can poison a queue. The +same unparsable message may cycle until the retry budget is exhausted, delaying +valid work behind it. For request/response systems, retrying may delay the +client feedback that would let the caller correct its serializer, schema, or +headers. + +##### A better approach + +Reject or divert malformed requests before retry. Treat parser failures, +unsupported content types, missing protocol envelopes, invalid wire formats, and +schema-incompatible payloads as terminal for that request. Return a clear client +error in request/response flows, or route the message to a dead-letter, +quarantine, or diagnostics path in asynchronous flows. + +Only after classification should a schedule be selected. Use schedules for +failures that can change with time: a temporarily unavailable +downstream service, a dropped connection, a rate limit response that permits +later retry, or an eventually consistent read. Then bound the recurrence with +operators such as `Schedule.recurs`, `Schedule.take`, or `Schedule.during`, and +use `Schedule.jittered` when many callers might retry at once. + +Keep the retry policy named after the retryable condition it serves, such as +"retry transient gateway failures" or "retry rate-limited sends briefly". Avoid +names like "retry malformed requests"; they encode the wrong operational +promise. + +##### Notes and caveats + +There are cases where a malformed response from another service is caused by a +transient deployment or proxy problem. Classify that separately as a bad +upstream response or temporary protocol mismatch, not as a malformed request from +your caller. Give it a narrow, bounded retry policy only if another attempt might +observe a corrected upstream. + +For inbound malformed requests, fail fast. A stricter boundary makes failures +more actionable: the caller sees that the request must be fixed, operators can +measure contract violations directly, and retry budgets remain available for +failures that time can actually resolve. + +#### 33.4 Retry non-idempotent side effects blindly + +Retrying non-idempotent side effects blindly is an anti-pattern because +`Schedule` controls timing, not replay safety. Non-idempotent means that running +the operation again can create another externally visible effect. + +##### The anti-pattern + +The problematic version wraps a mutating operation in a broad retry policy +because the failure looks transient or ambiguous. A timeout, dropped connection, +interrupted fiber, or `5xx` response around calls such as "capture payment", +"send receipt email", "submit order", or "create shipment" may mean the +dependency did nothing, or it may mean the dependency committed the side effect +before your service received the acknowledgement. + +The schedule may be bounded and well tuned. It may use backoff, jitter, +`Schedule.recurs`, `Schedule.take`, or `Schedule.during`. Those choices can +reduce load and make the retry budget visible, but they do not change the +external semantics of the operation. If each attempt creates a new side effect, +the policy is still unsafe. + +This often hides behind tidy infrastructure code. A generic HTTP client, queue +worker, or repository helper accepts a retry schedule and applies it uniformly to +every failure. Safe reads, deduplicated writes, validation errors, +authorization failures, and unsafe writes all start to look like the same +operational problem. + +##### Why it happens + +It usually happens when recurrence is designed before the domain contract. +`Schedule` is flexible enough to express many retry shapes, so it is tempting to +start with "retry transient failures" and leave replay safety for later. + +Non-idempotent effects also fail in uncomfortable ways. After a payment provider +times out, you may not know whether the card was charged. After an email +provider closes the connection, you may not know whether the message was queued. +Retrying feels productive because doing nothing feels like dropping work, but +blind retry can make the final state worse than the original uncertainty. + +##### Why it is risky + +Duplicate payments are the clearest failure. A customer can be charged twice +when the first attempt succeeded remotely but the acknowledgement was lost +locally. Backoff only changes when the second charge happens. + +Duplicate emails are also user-visible. A receipt, invite, password reset, or +notification may be delivered multiple times. Spacing the attempts can reduce +bursts, but it still asks the provider to create another delivery unless the +provider deduplicates by message identity. + +Duplicate orders and fulfillment requests can be expensive to unwind. A repeated +create call can allocate a second order number, reserve inventory twice, start +another shipment, or enqueue another warehouse task. + +The operational signal becomes harder to read as well. Metrics may show "retry +succeeded" even though the system created two side effects and observed only the +last acknowledgement. The schedule reports recurrence; it does not report +whether the dependency treated repeated attempts as the same logical operation. + +##### A better approach + +Require an idempotency key or equivalent deduplication mechanism before retrying +a mutating side effect. The key should identify the logical operation, not the +individual attempt. Every retry of "charge this invoice", "send this +notification", or "create this order" should carry the same stable key so the +downstream system can return the original result or reject the duplicate. + +Classify failures before applying the schedule. Retry only the cases where +another attempt is both useful and replay-safe: temporary unavailability, rate +limiting, connection resets, or ambiguous transport failures for an operation +protected by idempotency. Do not retry malformed requests, failed validation, +forbidden access, declined payments, unsubscribed recipients, or business-rule +rejections. + +Keep the retry policy narrow and named after the operation it protects. A policy +such as "retry idempotent payment capture briefly" communicates more than a +generic "remote API retry". Combine backoff or spacing with explicit limits such +as `Schedule.recurs`, `Schedule.take`, or `Schedule.during`, and add jitter when +many callers may retry the same dependency. + +When the dependency cannot deduplicate, choose a different recovery path. Record +the uncertain outcome, reconcile through provider status APIs, use an outbox +with a uniqueness constraint, require operator review, or return a clear pending +state to the caller. + +##### Notes and caveats + +Idempotency has to be end-to-end. A stable key in your request is not enough if +an intermediate service drops it, generates a fresh one per attempt, or +deduplicates for a shorter window than your retry and reconciliation workflow +requires. + +Some operations are naturally idempotent because they set a resource to a known +state or use a deterministic identifier. Others can be made idempotent with +request keys, unique constraints, compare-and-set updates, outbox records, or +provider-specific client tokens. If neither is true, treat retries as a product +and operations decision, not as a schedule choice. + +Use `Schedule` to control recurrence only after the domain has made recurrence +safe. Backoff, spacing, jitter, and retry limits are load-shaping tools. They are +not a substitute for idempotency. + +#### 33.5 Retry without error classification + +Retrying without error classification is an anti-pattern because it asks a +timing policy to make a domain decision. + +##### The anti-pattern + +The problematic version starts with a shared retry policy before the error model +is understood. A client, worker, repository, or service helper wraps an +operation in a broad schedule because some failures are retryable. The same +policy then handles timeouts, rate limits, validation errors, authorization +failures, malformed requests, declined payments, duplicate-key errors, and +invariant violations. + +The schedule may look responsible. It might use exponential backoff, fixed +spacing, a recurrence cap, an elapsed-time budget, and jitter. Those controls +shape retry traffic, but they do not classify the error. A bounded retry of a +permanent failure is still a delayed permanent failure. + +This is easy to miss when the schedule is hidden in infrastructure code. A +helper named "retry remote calls" can retry every typed failure from a remote +call, even though only a small subset represent temporary infrastructure +conditions. + +##### Why it happens + +It usually happens when recurrence is designed before the failure taxonomy. A +taxonomy is the small set of categories you use to decide what an error means. +`Schedule` is convenient and composable, so it is tempting to choose a reusable +policy before answering the more important question: which failures may safely +be attempted again? + +It also happens when the retry boundary is too far from the code that +understands the domain. A low-level HTTP wrapper can see that the call failed, +but it may not know whether the failure means "the network dropped", "the token +is revoked", "the payload violates the schema", "the account is disabled", or +"the provider may already have committed the side effect". + +Metric pressure can reinforce the mistake. Retrying can make a flaky operation +appear healthier because a later attempt succeeds. That is useful for genuine +transient failures. It is misleading when retry hides a permanent caller bug, a +configuration problem, or a fatal state transition. + +##### Why it is risky + +Permanent failures consume retry budgets, queue capacity, connection slots, +logs, and downstream quota even though another attempt cannot make the request +valid. Backoff and jitter reduce synchronization, but they still spend capacity +on work that should have failed fast. + +Feedback is delayed. A caller that sent invalid input should learn that +immediately. A service using revoked credentials should surface an authorization +failure. A deployment with missing configuration should fail in a way operators +can recognize. A retry schedule can bury those signals under an exhausted retry +budget. + +Some failures occur after the request has left your process. If a payment +capture, order creation, message send, or external write times out, the remote +side may already have committed the action. Retrying without first classifying +the outcome and checking idempotency can duplicate work outside the process. +`Schedule` can delay and limit those attempts; it cannot make them replay-safe. + +Fatal failures are not "permanent but harmless". They often mean the workflow +has lost an invariant, observed corrupted state, or reached an ambiguous +external state that requires reconciliation. Treating those failures like +transient unavailability can continue a workflow after it should stop. + +##### A better approach + +Classify failures before retrying. Keep the classification close to the boundary +that understands the operation, and make the categories explicit enough to +review: + +- transient: retryable because time may change the result +- permanent: not retryable for this request because the request or caller must + change +- fatal: not retryable in this workflow because continuing may be unsafe or + misleading + +Only the transient category should reach the retry schedule. The schedule then +answers a smaller question: given that this failure is retryable, how should +recurrence proceed? Use schedule operators for timing and termination, such as +recurrence limits, elapsed budgets, backoff, spacing, and jitter. Do not use +them as a substitute for deciding whether the failure belongs in the retry path. + +Return permanent failures directly. They are useful information, not retry +candidates. Route fatal or ambiguous failures to the recovery path that +preserves correctness: reconciliation, dead-letter handling, operator +intervention, idempotency lookup, status polling, or a pending state. Those paths +may use their own schedules, but only after the failure has been reclassified +into a specific recovery action. + +Name the policy after the classified condition it protects, such as "retry +transient object-storage reads briefly" or "retry rate-limited status fetches +within the caller budget". Avoid names like "generic retry" or "retry all +downstream errors"; they hide the decision that matters most. + +##### Notes and caveats + +Classification does not have to be elaborate, but it has to happen before retry. +A small predicate or tagged error model is usually enough to separate retryable +transport and availability failures from request, authorization, configuration, +business, and fatal workflow failures. + +Some errors change category after context is added. A timeout on an idempotent +status read may be transient. A timeout after submitting a non-idempotent write +may be ambiguous or fatal until the system reconciles the external state. The +same low-level symptom can require different retry behavior depending on the +operation. + +Use `Schedule` after the operation has proven that another attempt is meaningful +and safe. Classification decides whether retrying is allowed. The schedule +decides when retrying happens and when the retry budget is exhausted. + +### 34. Retrying Forever + +#### 34.1 Missing retry limits + +##### The anti-pattern + +A retry policy describes when another attempt may happen but never says when +to stop. The delay shape can look reasonable: exponential backoff, fixed +spacing, or a shared "retry transient errors" schedule. The problem is that +timing is not a budget. + +If the effect never succeeds, the schedule can keep producing retry decisions +for the lifetime of the fiber. That turns a temporary recovery mechanism into +an open-ended workload. + +##### Why it happens + +The code answers "how long should we wait between failures?" before it answers +"how many failures are acceptable?" Delay is easy to tune locally; termination +requires understanding the caller, the dependency, and the side effect. + +This also appears when one shared policy is reused across operations with +different risk profiles. A safe read, an idempotent write, and a non-idempotent +side effect should not inherit the same retry lifetime. + +##### Why it is risky + +Unbounded retries convert one failure into continuing operational load. A down +dependency receives more traffic while it is least able to handle it. A +malformed request, expired credential, or authorization failure is repeated even +though another attempt cannot make it valid. + +Unsafe side effects are worse. If a remote service completed the work but failed +before returning success, an unbounded retry can perform the operation more than +once. + +The caller also loses a timely exhausted-retry error. During an incident, these +background retries can consume connection pools, queue slots, rate-limit budget, +and log volume that operators need for recovery. + +##### A better approach + +Put the stopping rule in the policy. Use `Schedule.recurs(n)` when the contract +is a maximum number of retries; with `Effect.retry`, the original attempt runs +first and `Schedule.recurs(3)` permits at most three retries after it. + +When the useful part is the delay shape, keep it and cap it with +`Schedule.take(n)`. This works for schedules such as `Schedule.exponential`, +`Schedule.spaced`, or `Schedule.fixed`, which otherwise continue to produce +recurrence decisions. + +When the contract is elapsed time, use `Schedule.during(duration)`. It continues +only while the schedule's elapsed recurrence window remains within the supplied +duration. Startup checks and short dependency recovery windows usually need +this kind of time budget. + +Name policies after both cadence and limit: `retryHttp503ThreeTimes`, +`retryTokenRefreshForTenSeconds`, or `retryUploadWithCappedBackoff`. A name like +`exponentialRetry` describes the curve but hides the operational promise. + +##### Notes and caveats + +An attempt limit is not error classification. Validation errors, authorization +failures, malformed requests, and known fatal responses should usually fail +without retrying at all. + +Count limits and time limits answer different questions. `Schedule.recurs` and +`Schedule.take` bound recurrence count; `Schedule.during` bounds elapsed +schedule time. Production policies often need both. + +#### 34.2 Missing time budgets + +##### The anti-pattern + +A retry policy has a delay curve and a retry count, but no elapsed budget. It +looks bounded because the number of retries is finite. It is still open-ended in +the dimension the caller often cares about: total time. + +The caller pays for every delay plus every failed attempt. If each attempt can +run near its own timeout, or if a fixed delay later becomes exponential, a small +retry count can still exceed the useful window for a request, lease, startup +path, or recovery workflow. + +Retry counts are useful. The mistake is treating a count as a time budget when +the operation has a deadline. + +##### Why it happens + +Attempt counts are easy to review. "Retry five times" looks concrete, while +"stay within two seconds" requires thinking about caller ownership, failure +latency, and how long the result remains useful. + +It also comes from mixing up different guards: + +- a delay cap limits one pause before the next recurrence +- `Schedule.recurs(n)` limits scheduled retries after the original attempt +- `Schedule.take(n)` limits schedule outputs +- `Schedule.during(duration)` limits the schedule's elapsed recurrence window +- a timeout around the effect limits an individual in-flight attempt + +They protect different things. Replacing one with another changes the contract. + +##### Why it is risky + +Attempt counts do not compose cleanly with variable latency. Five fast failures +may be acceptable for an interactive caller. Five slow failures may hold a +request, worker, lock, connection, or startup path long after useful work is no +longer possible. + +Counts also age badly as the schedule evolves. A later change from fixed spacing +to exponential backoff, a higher per-attempt timeout, or a larger delay cap can +multiply total elapsed time while the visible count stays the same. + +Missing time budgets create retry tails: long, low-visibility periods where work +is still waiting, resources are still held, and fallback paths are delayed. + +##### A better approach + +Keep the attempt count when it is useful, but add an elapsed budget whenever the +caller owns a maximum retry window. In Schedule terms, compose the cadence with +`Schedule.during(duration)` using `Schedule.both`. The cadence decides when +another retry may happen. The `during` side decides whether the elapsed schedule +window is still open. Because `Schedule.both` continues only while both +schedules continue, retrying stops when either guard is exhausted. + +Prefer names that state the promise: `retryPaymentLookupForTwoSeconds`, +`startupConfigRetryBudget`, or `webhookDeliveryRetryWindow` communicate more +than `retryPolicy`. + +Use an attempt count to prevent excessive work inside the budget. Use an elapsed +budget to protect the caller from waiting too long. Use a per-attempt timeout +when one attempt needs its own maximum duration. These are complementary guards, +not alternatives. + +##### Notes and caveats + +`Schedule.during` is evaluated at schedule decision points. It does not +interrupt an attempt that is already running, and it is not a replacement for a +timeout around the effect itself. + +With `Effect.retry`, the first attempt runs immediately. `Schedule.recurs(n)` +allows up to `n` retries after that original attempt, not `n` total executions. +If the operation succeeds, the retry schedule is no longer consulted. + +Elapsed budgets make failure earlier and clearer. That is the point: callers can +fall back, return a timely error, enqueue background work, or release resources +instead of waiting through a retry tail. + +#### 34.3 Unbounded backoff chains + +##### The anti-pattern + +An unbounded backoff schedule is used as the whole retry policy. A policy based +on `Schedule.exponential("200 millis")` looks conservative because each attempt +waits longer than the last. In `Schedule`, `exponential` always recurs. By +itself it has no maximum attempt count, no elapsed budget, and no maximum single +delay. + +Backoff changes pressure; it does not create a recovery contract. After enough +attempts the next sleep may be minutes or hours away, but the work is still +pending and may retry after the caller, job, or incident process expected a +decision. + +##### Why it happens + +It happens when "back off" is treated as "bound the retry." Exponential growth +reduces pressure on a dependency, but it does not decide when the original +operation has failed. + +A shared backoff can also leak across workflows. A queue reconnect loop, a +user-facing request, a startup probe, and a control-plane mutation may all need +backoff, but they should not inherit the same lifetime. + +##### Why it is risky + +The long tail is the main risk. Early attempts are visible and close together; +later attempts are far apart. A failing job can look quiet even though it is +still scheduled to act. Ownership becomes ambiguous: the caller may have moved +on, the worker may still hold state, and the next retry may run after the +surrounding context is stale. + +A very large next wait can also look like a stuck process. If the operation +eventually retries, it may run after credentials, leases, idempotency windows, +request deadlines, or deployment assumptions have changed. For unsafe side +effects, a late retry can be worse than a clear failure. + +Backoff does not eliminate fleet load. Many callers using the same unbounded +policy can accumulate delayed work. Without jitter, similar failures can retry +together. Without a deadline, the backlog can persist through recovery. + +##### A better approach + +Treat backoff as cadence, not limit. Start with the retryable case, then add +explicit bounds that match the workflow: + +- use `Schedule.recurs` when the contract is a maximum number of retries +- use `Schedule.during` when the contract is a wall-clock retry budget +- use `Schedule.modifyDelay` with `Duration.min` when each sleep needs a maximum cap +- use `Schedule.jittered` when many fibers or processes may run the same policy together + +An exponential cadence is appropriate for temporary overload. A production +policy also says when to stop and how long any single sleep may become. That +gives the caller an exhausted-retry outcome instead of leaving the operation in +a distant future. + +Prefer names that include the bound, such as "retry inventory reads for up to +twenty seconds" or "reconnect with ten capped backoff attempts." A name like +`exponentialRetry` describes the curve but not the promise. + +##### Notes and caveats + +Caps and deadlines solve different problems. A delay cap prevents one +recurrence from sleeping too long. A deadline or recurrence limit decides when +the retry as a whole is over. Most production policies need both. + +Use schedule combinators deliberately. `Schedule.both` uses intersection +semantics: it continues only while both schedules continue and uses the maximum +delay. That is usually what you want when combining cadence with a limit. +`Schedule.either` uses union semantics and can accidentally preserve an +unbounded tail. + +A bounded policy may surface failures sooner. That is expected when the old +behavior only delayed a decision. If a workflow truly needs indefinite +background recovery, make it visible with ownership, cancellation, +observability, jitter where appropriate, and a bounded per-attempt delay. + +#### 34.4 Operationally invisible infinite retries + +##### The anti-pattern + +A retry schedule is treated as a private implementation detail. The effect +fails, retries, and nothing outside the fiber can tell whether this is attempt +two or attempt two thousand. The caller sees latency or eventual failure. +Operators see downstream symptoms: repeated API calls, elevated queue age, +extra database reads, or a job that never completes. + +This often appears as a tidy shared policy such as unbounded +`Schedule.exponential("200 millis")` or `Schedule.spaced("1 second")`. Those +schedules are real tools. The policy is incomplete when it has no retry budget, +no elapsed time budget, no classification of retryable failures, and no signal +for each retry attempt. + +##### Why it happens + +The schedule is chosen before the operation has an operational contract. The +code decides to retry a transient failure, then postpones the harder questions: +which errors are transient, how long the caller may wait, whether the side +effect is idempotent, and what signal should be emitted on each recurrence. + +`Schedule` makes recurrence compositional, so an unbounded policy is easy to +pass to `Effect.retry`. That composability is useful, but the absence of a bound +is still a policy. If no one combines the schedule with `Schedule.recurs`, +`Schedule.take`, `Schedule.during`, or another stopping condition, the retry can +continue as long as the fiber is alive. + +##### Why it is risky + +Invisible retries hide both product failures and infrastructure failures. A +malformed request remains malformed. A revoked credential will not become valid +through backoff. A non-idempotent write can duplicate work outside the process. +A downstream outage can be amplified by every caller running the same loop. + +The risk is worse during incidents. If retry attempts are not counted, logs lack +attempt numbers and causes, and metrics do not expose retry volume, the team +cannot distinguish a few slow operations from many permanently failing ones. +Without elapsed-time or attempt limits, retry traffic may continue after the +business deadline has passed. + +Backoff can also create false confidence. `Schedule.exponential` and +`Schedule.fibonacci` reduce retry pressure over time, but they do not make the +retry finite. `Schedule.jittered` spreads callers out, but it does not provide a +budget. Delay is not observability, and it is not termination. + +##### A better approach + +Make the retry contract explicit before choosing the cadence. Classify failures +first, retry only cases that are expected to recover, and give the policy a +count budget, a time budget, or both. Combine the delay policy with a stopping +policy such as `Schedule.recurs`, `Schedule.take`, or `Schedule.during`. When +many callers may retry at the same time, add `Schedule.jittered`, but keep the +limits visible. + +Make each recurrence observable. `Schedule.tapInput` can record the failure that +caused a retry. `Schedule.tapOutput` can record schedule output, such as delay +or recurrence count. Where elapsed time matters, use a time-limited policy or +compose with `Schedule.elapsed` so logs and metrics can answer how many attempts +happened, why they happened, how long the operation has been retrying, and when +the policy stopped. + +Prefer metrics with stable dimensions over ad hoc log volume. Useful signals +include retry attempts by operation and error class, retry exhaustion counts, +elapsed retry duration, selected-delay histograms, and the number of fibers +currently waiting to retry. Logs should carry operation name, classified error, +attempt number, elapsed time, and the final exhaustion event. + +Name retry policies after their operational promise: +`retryHttp503ForThirtySeconds`, `retryConnectionResetFiveTimes`, or +`pollUntilReadyWithinStartupBudget` is clearer than `defaultRetrySchedule`. + +##### Notes and caveats + +Some systems deliberately retry forever, such as long-lived background workers +or supervisors. That is acceptable only when the retry is visible and externally +controllable: structured logs, metrics, alerting, backoff, jitter where +appropriate, and a documented shutdown or cancellation path. + +Do not use infinite retry to hide a request-scoped failure from a caller that +needs a timely answer. A bounded retry may surface errors sooner; that is the +point when work has left its useful window. + +#### 34.5 Background loops with no escape hatch + +##### The anti-pattern + +A recurring effect is started in the background and the schedule is treated as +the whole lifecycle policy: + +- repeat every few seconds forever +- retry reconnects forever +- poll until success, but with no timeout +- log failures and continue indefinitely +- fork the loop and never retain, supervise, or interrupt the fiber + +`Schedule.forever` and unbounded `Schedule.spaced` are valid timing tools. The +anti-pattern is using them without deciding who owns the loop, when it stops, +how it is interrupted, how failures are bounded, and how operators can tell +whether it is making progress. + +This often appears as a fire-and-forget maintenance loop: refresh a cache, renew +a lease, publish metrics, scan a queue, reconcile state, or reconnect a client. +It works in local testing because the process is short lived. In production, the +same loop can outlive request cancellation, deployment drains, lost leadership, +disabled tenants, expired credentials, or a downstream outage. + +##### Why it happens + +Recurrence is designed before ownership. A schedule is a small value, so it is +easy to attach `Effect.repeat` or `Effect.retry` to an operation and move on. +The code reads as intentional because the delay is named, but the lifecycle is +not. + +Another cause is confusing "runs forever" with "is managed forever". +Schedules describe recurrence. They do not decide that a loop should outlive a +request, survive a scope closing, ignore shutdown, or keep running after its +business purpose has disappeared. + +The problem is worse when retry and repeat are mixed together. A background +poller may repeat forever, and each iteration may retry forever on failure. That +creates nested unbounded recurrence: the outer loop never ends, and the inner +failure path has no budget. + +##### Why it is risky + +The risk is not just CPU. A loop without interruption can keep resources alive +after their owner is gone: connections, subscriptions, queue leases, cache +handles, tenant state, and fibers. If it performs external work, it can keep +sending requests after the feature was disabled or the caller timed out. + +An unbounded loop also hides failure. If every error is logged and the loop +continues, the system may look available while doing no useful work. If failures +are retried forever, the final error never reaches a caller and the only visible +symptom may be delayed shutdown, growing logs, repeated downstream traffic, or a +slow increase in background fibers. + +Budgets make the loop's cost reviewable. Without a budget, it can spend +unlimited time reconnecting, refreshing, polling, or reconciling. Without +concurrency and queue limits, it can accumulate more work than the system can +drain. Without observability, operators cannot distinguish healthy idle work +from a stuck loop repeating the same failure. + +##### A better approach + +Design the loop as a managed process, then choose the schedule. Give every +background loop a lifecycle owner and make interruption part of the design. If +the loop belongs to a request, tenant, lease, subscription, or service scope, it +should stop when that owner stops. If it is process-level infrastructure, it +should participate in shutdown and expose enough state to be supervised. + +Add explicit recurrence limits where failure is possible. Use count limits such +as `Schedule.recurs` or `Schedule.take` when an operation only deserves a fixed +number of retries or repeats. Use time budgets such as `Schedule.during` when +the operation may wait for a condition but should not wait forever. Use +`Schedule.while` when schedule metadata such as attempt, input, output, or +selected delay decides whether another recurrence is allowed. + +Keep the forever part narrow. It can be reasonable for an outer service loop to +repeat for the lifetime of the service, but inner recovery loops should still +have budgets. A cache refresher may run for the service lifetime while one +refresh attempt has a small retry policy. A reconnecting client may be owned by +a scope while each connection attempt has bounded backoff. A poller may run for +an active subscription while each poll has a timeout and terminal state +handling. + +Make observability part of the schedule boundary. `Schedule.tapInput` and +`Schedule.tapOutput` are useful places to record retry inputs, recurrence counts, +selected delays, and other schedule outputs. Metrics and logs should answer at +least these questions: how many loops are running, when did each last make +progress, how many consecutive failures has it seen, what delay is it using, and +which owner or tenant is responsible for it. + +##### Notes and caveats + +A loop that is intended to run for the whole process lifetime can still use an +unbounded schedule. The requirement is not "never use `Schedule.forever`". The +requirement is that forever has an owner, an interruption path, bounded failure +handling inside the loop, and production signals that show whether it is doing +useful work. + +Do not rely on jitter as the escape hatch. `Schedule.jittered` spreads recurrence +delays, which helps when many instances might synchronize, but it does not stop +a loop, cap its work, or report that it is unhealthy. Jitter is load shaping, +not lifecycle, budget, or observability. + +### 35. Polling and Jitter Mistakes + +#### 35.1 Poll every 100ms without need + +##### The anti-pattern + +A fixed 100 millisecond cadence is chosen before the code asks how quickly the +answer can change or how expensive each check is. The schedule reads like a +small detail, but `Schedule.spaced("100 millis")` keeps recurring until another +condition stops it. + +A batch job that finishes in minutes, an eventually consistent index, a mostly +empty queue, or a rate-limited status endpoint rarely benefits from ten checks +per second. One loop looks small. Ten loops already mean roughly one hundred +checks per second; across services, tenants, browser tabs, or worker fibers, the +extra polls mostly rediscover the same state. + +##### Why it happens + +Responsiveness becomes the only scheduling goal. A 100 millisecond interval +feels fast and is easy to remember, so it gets copied into polling code even +when no user is waiting, no service-level objective depends on that latency, and +the observed value cannot change that quickly. + +Local testing also biases toward tight intervals. Short polling makes demos and +manual verification feel snappy. If that value reaches production unchanged, +the schedule documents impatience rather than operational intent. + +##### Why it is risky + +The direct cost is load. Every recurrence wakes a fiber, runs the effect, and +usually touches another subsystem. If the poll performs network I/O, storage +I/O, logging, tracing, or metrics, the system pays those costs even when nothing +changed. + +The indirect cost is worse during incidents. When a dependency slows down, +aggressive polling adds more concurrent requests to the dependency that is +already struggling. When many callers share the same fixed interval, their +checks can align and produce bursts. When the loop is unbounded, the load keeps +going until interruption, success, or an explicit stopping rule ends it. + +Fast polling can also hide a missing domain signal. If the right design is an +event, callback, subscription, queue, or "try once and come back later" flow, a +100 millisecond loop makes the absence of that signal look acceptable while +charging the system continuously. + +##### A better approach + +Choose the interval from the domain first. Ask how quickly the observed value +can realistically change, who is waiting for it, what each check costs, and what +the maximum acceptable polling budget is. If the answer is "nobody needs this in +100 milliseconds", start with seconds, not milliseconds. + +For steady polling, prefer a wider `Schedule.spaced` interval that matches the +freshness requirement. For recovery or readiness checks, prefer a backoff shape +such as `Schedule.exponential` or `Schedule.fibonacci` so repeated misses become +less frequent. For any policy that is not meant to run forever, add an explicit +bound with `Schedule.take`, `Schedule.recurs`, or `Schedule.during`. + +When many processes may poll the same dependency, add jitter after the base +cadence is correct so the fleet does not check in lockstep. Jitter is not a +license to keep an interval too small; it only spreads otherwise reasonable +traffic. + +##### Notes and caveats + +There are valid 100 millisecond schedules. They belong near cheap local +coordination, short-lived startup readiness, tests, and bounded user-facing +waits where that latency is part of the requirement. Even then, make the stop +condition visible. + +`Schedule.spaced("100 millis")` controls the delay between recurrences; it does +not make the work cheap, cancel stale demand, or limit the total number of +checks. If the loop is meant to protect a dependency, the schedule should show +that protection through wider spacing, backoff, jitter, and a clear bound. + +#### 35.2 Poll large fleets in sync + +##### The anti-pattern + +Every instance gets the same repeat schedule and starts from the same lifecycle +event: deploy, boot, leader change, cache flush, or incident recovery. A plain +fixed or spaced interval reads as "poll every 30 seconds." Across a fleet it can +mean "ask the same dependency at once." + +This is easy to miss with background polling. One worker polling every few +seconds is usually fine. The load shape appears when many identical processes +run the same policy and thousands of workers turn a status endpoint, queue +broker, database, or control plane into the bottleneck even though average +request rate looks acceptable. + +##### Why it happens + +The schedule is designed for one process instead of the fleet. +`Schedule.spaced("30 seconds")` waits after each successful poll completes +before the next one. `Schedule.fixed("30 seconds")` maintains a constant +interval, and if work takes longer than the interval the next run can happen +immediately rather than piling up missed runs. Both are useful; neither spreads +identical clients by itself. + +Deployments make the synchronization worse. If every worker starts around the +same time, the first poll aligns. If the work duration is similar, the following +polls can stay aligned. A dependency outage can also re-synchronize clients when +they all begin polling again after the same recovery signal. + +##### Why it is risky + +The risk is burst load, not just total load. A backend sized for a steady 10,000 +requests per minute may still fail if most of those requests arrive in a narrow +window every minute. Synchronized polling can create noisy metrics, queue depth +oscillation, periodic database contention, rate-limit bursts, and incident +feedback loops where every client checks more aggressively at the worst time. + +It also hides the cause. Operators may see a slow poll endpoint or a database +spike every 30 seconds without an obvious offender because no single instance is +violating its local schedule. + +##### A better approach + +Design the polling policy as a fleet policy. Choose the base cadence from the +freshness requirement and the downstream cost, then add spreading when many +instances may run it. In Effect, `Schedule.jittered` keeps the same general +cadence while randomly adjusting each computed delay between 80% and 120% of the +original delay. + +Use jitter for runtime polling loops where exact wall-clock alignment is not a +requirement. Keep it visible in the schedule rather than hiding randomness in +the poll effect, so the operational contract remains reviewable: the cadence +states how often the work should happen, and jitter states that the fleet should +not wake up in lockstep. + +If the dependency needs tighter protection, combine jitter with a slower +cadence, a concurrency limit, caching, server-side push, or a poll response that +tells clients when to check again. Jitter smooths synchronization; it does not +make an expensive polling design cheap. + +##### Notes and caveats + +Do not add jitter where precise cadence is the point of the workflow. Heartbeats, +billing boundaries, time-bucketed aggregation, and protocol-level leases may +need explicit timing semantics. For those cases, reduce fleet pressure with +partitioning, ownership, or a separate coordination mechanism instead of random +delay. + +Jitter is also not a retry limit or a backpressure mechanism. If the poll loop +can run forever, decide whether that is intentional and document the cost. If the +loop is only useful during a window, pair the cadence with an explicit limit such +as a count or elapsed-time budget. + +#### 35.3 Poll when a push-based model would be better + +##### The anti-pattern + +Polling is used as the default integration shape. A worker asks a remote API for +status every few seconds. A dashboard refreshes a large query on a fixed +cadence. A service repeatedly scans a table to discover new work. A fleet of +consumers checks for "anything changed?" even though the upstream system could +send a webhook, publish an event, expose a subscription, or enqueue work when +state changes. + +The schedule may look careful. It might use `Schedule.spaced` for a predictable +delay, `Schedule.fixed` for a wall-clock cadence, `Schedule.jittered` to avoid +fleet synchronization, or `Schedule.take` / `Schedule.recurs` to avoid running +forever. Those are useful controls for legitimate polling. They do not change +the fact that every poll is still a speculative read. `Schedule` can make the +loop slower, bounded, jittered, or easier to review; it cannot turn repeated +guessing into an event-driven design. + +##### Why it happens + +Polling is easy to add locally. The consumer can ship without asking the +producer for a new contract, provisioning a queue, validating webhook +signatures, or designing event delivery semantics. `Schedule` then makes the +loop look intentional because the cadence is explicit and composable. + +That convenience can hide the architectural question: who has the information +first? If the producer observes the change, the producer is usually the better +place to emit the signal. The consumer's schedule is only guessing at the right +time to look. + +##### Why it is risky + +The risk is not only extra requests. Polling creates a freshness-versus-load +tradeoff that push-based systems often avoid. A faster cadence reduces stale +reads but increases API traffic, database scans, cache churn, rate-limit +pressure, log volume, and cost. A slower cadence protects dependencies but makes +users and downstream workflows wait for changes the system already knows about. + +Polling also fails poorly at scale. During deploys, outages, or incident +recovery, many consumers can resume the same polling loop at once. Jitter can +smooth that pattern, but it cannot remove the repeated work. Backoff can protect +a dependency during failure, but it also makes change detection slower. A count +or time budget can stop the loop, but it may stop before the change arrives. +These are symptoms of using a timer where a message would describe the real +business event. + +##### A better approach + +Choose the communication model before choosing the schedule. If another system +owns the state transition, prefer a push contract: webhooks for cross-service or +third-party notifications, an event stream for durable state changes, a queue +for work that must be claimed and processed, or a subscription/channel when the +client needs live updates. Those designs move the recurrence problem out of the +consumer and into delivery, acknowledgement, replay, deduplication, and +backpressure mechanisms that are built for change notification. + +Use `Schedule` when polling is genuinely the right boundary: a remote service +only exposes a status endpoint, the operation is short-lived and user-scoped, a +legacy dependency cannot emit events, or the poll is a safety net for missed +signals. In those cases, make the compromise explicit. Pick a cadence from the +freshness requirement and downstream cost, add jitter when many clients may run +the same loop, and add a visible termination condition with `Schedule.take`, +`Schedule.recurs`, or `Schedule.during` when the loop should not live forever. + +When polling is only a fallback, document that in the schedule name and metrics. +The primary path should be the webhook, event, queue, or subscription; the +scheduled loop should repair gaps rather than define normal operation. + +##### Notes and caveats + +Push-based systems are not free. Webhooks need authentication, idempotency, +retry handling, and dead-letter visibility. Event streams and queues need +retention, consumer ownership, replay strategy, and operational tooling. Those +costs are real, but they match the problem of delivering changes. A polling +schedule mostly controls how often the consumer asks a question. + +Keep polling when the producer cannot push, when eventual freshness is enough, +or when periodic reconciliation is deliberately part of the reliability model. +Do not use a nicer `Schedule` to avoid fixing an integration contract that +should be event-driven. + +#### 35.4 Adding jitter where precise cadence matters + +Jitter is for spreading load, not for preserving precision. Avoid it when the +schedule's cadence is part of a protocol, measurement, lease, or user-facing +promise. + +##### Anti-pattern + +A deterministic cadence, such as `Schedule.fixed`, `Schedule.spaced`, or +predictable backoff, is piped through `Schedule.jittered` because jitter sounds +safer in production. + +That changes the policy. `Schedule.jittered` randomly adjusts each computed +delay to a value between `80%` and `120%` of the original delay. A five-second +cadence becomes a bounded range around five seconds, not an exact interval. + +The mistake often hides in shared helpers: a "production schedule" adds jitter +to retries, pollers, heartbeats, refreshes, and maintenance tasks. It may reduce +synchronized bursts for some workloads, but it also injects timing variance into +workloads whose correctness depends on predictable recurrence. + +##### Why it happens + +Jitter is associated with resilience because it helps clustered systems avoid +herd effects, where many callers hit the same dependency at the same time. That +benefit is real, but it is a fleet-level load decision, not a default property +of every schedule. + +`Schedule` values document recurrence. For fixed heartbeats, user-visible +polling intervals, sampling loops, lease renewals, and virtual-time tests, the +absence of jitter is part of the contract. + +##### Why it is risky + +Jitter can violate external contracts. Protocol heartbeats, lock refreshes, +timeout probes, and lease renewals often rely on a specific margin. A delay that +is 20% later than the base delay may be fine for retry traffic, but wrong for a +renewal loop sized around a fixed deadline. + +It can also weaken observability. Sampling, load tests, diagnostic probes, and +periodic reports often rely on deterministic spacing so measurements remain +comparable. Jitter can make a graph look smoother while making the data less +faithful to the question being measured. + +The variance can become visible to users too. If the interface says "refreshing +every 5 seconds", a jittered schedule no longer implements that exact promise. +Tests become less precise for the same reason: deterministic schedules can use +exact virtual-time advancement, while jittered schedules need range assertions +and controlled randomness. + +##### A better approach + +Choose the schedule shape that states the timing requirement and leave it +unjittered when precision matters. Use `Schedule.fixed` when work should align +to a fixed interval. Use `Schedule.spaced` when each run should wait a stable +gap after the previous run. Use deterministic `Schedule.exponential` when the +retry curve should be predictable. Add `Schedule.recurs`, `Schedule.take`, or +`Schedule.during` for explicit bounds. + +Reserve `Schedule.jittered` for cases where synchronized callers are the bigger +problem than exact per-caller timing: many service instances retrying the same +dependency, many clients polling the same resource, or many workers waking after +a shared outage. In those cases, the `80%` to `120%` range is the feature. + +Name schedules after the behavior they promise. A name like +`leaseRenewalCadence` should make jitter look suspicious. A name like +`jitteredReconnectBackoff` makes the tradeoff explicit. + +##### Caveats + +Jitter changes only the delay chosen for the next recurrence. It does not add a +retry limit, make an unsafe operation safe, classify errors, or enforce a rate +cap. If a precise schedule overloads a dependency, first check cadence, +concurrency, and admission control. Jitter may still help a coordinated fleet, +but it should not blur a timing contract the program depends on. + +#### 35.5 Using jitter to mask a deeper overload problem + +Jitter can smooth synchronized recurrence, but it cannot decide whether the +system should accept more work. Random timing is not capacity control. + +##### Anti-pattern + +Synchronized load appears, and jitter becomes the main fix. A hot endpoint is +retried by many callers, a poller hammers a downstream service, or a batch job +fans out more work than the dependency can accept. `Schedule.jittered` shifts +each delay within its `80%` to `120%` band, so a spike becomes a wider plateau. + +That graph can look better while the overload remains. If callers still retry +too many times, pollers still have no terminal condition, or workers still admit +unbounded concurrency, the system is still asking the dependency to do more than +it can handle. + +##### Why it happens + +Jitter is cheap to add and often produces an immediate visual improvement. A +jittered exponential backoff reads as more production-ready than the same +backoff without jitter, so it is tempting to stop there. + +The missing question is whether the system should be doing the work at all. +Jitter changes when the next recurrence happens. It does not classify +non-retryable failures, cap retry budgets, bound concurrency, queue work behind +backpressure, reject excess demand, or enforce a shared rate limit. + +##### Why it is risky + +Randomized overload is still overload. During a partial outage, jitter can keep +steady pressure on a dependency that needs room to recover. The system may avoid +sharp retry waves while still consuming connection pools, worker slots, request +budgets, and operator attention. + +It also hides the real contract from the code. A reader sees a jittered schedule +and may assume the retry or polling policy is operationally safe. If the schedule +has no recurrence limit, no elapsed budget, no input classification, and no +coordination with admission control, the safety is only cosmetic. + +Jitter can make telemetry harder to interpret as well. The failure is no longer +a clean synchronized spike; it is smeared across time. That can delay the more +important fix: reducing admitted demand, preserving capacity for healthy work, +or making callers fail fast when the system is already saturated. + +##### A better approach + +Treat jitter as the last timing refinement on top of an already bounded policy. +First decide which work is allowed to recur, how many times it may recur, how +long the recurrence window may stay open, and what should happen when the system +is saturated. + +Use schedule operators for the recurrence contract. `Schedule.recurs(n)` or +`Schedule.take(n)` makes a count budget visible. `Schedule.during(duration)` +makes the elapsed recurrence window visible. `Schedule.both` can combine cadence +with a count or time budget so recurrence continues only while both schedules +continue. Add `Schedule.jittered` only when many callers may otherwise align on +the same delay boundaries. + +Use the right non-schedule mechanism for overload control. Bound concurrency for +work that consumes scarce worker or connection capacity. Use queues or streams +with backpressure when producers must slow down behind consumers. Use rate +limiting or admission control when excess demand should wait, be rejected, or +receive a clear retry-after signal. Use load shedding when preserving service +health matters more than accepting every request. + +The schedule should then describe retry or polling behavior, not carry the full +burden of system protection. A good policy might be jittered, but it is safe +because it is narrow, bounded, and coordinated with capacity controls. + +##### Caveats + +Do not remove jitter from a fleet-wide retry policy just because it is not a +capacity fix. Jitter is still useful for avoiding synchronized recurrence and is +often the correct addition to exponential or spaced delays. + +The caveat is ownership. If the system is overloaded, the owner of the policy +must decide what demand is admitted, queued, slowed, or rejected. Jitter can make +that decision less noisy; it cannot make the decision for you. + +## Part X — Choosing the Right Recipe + +### 36. Recipe Selection Guide + +#### 36.1 “I need to retry a flaky call” + +Use this entry when an operation may succeed if tried again, but only under a +bounded, explicit retry policy. + +##### What this section is about + +Start with the call shape, not the combinator: + +- A very short local race, such as a warm cache, leader election handoff, or just-created resource, usually wants a small constant delay. +- A remote dependency that may be overloaded usually wants backoff. +- A call made by many fibers, processes, or nodes usually wants jitter. +- A user-facing call usually wants a short attempt or elapsed-time budget. +- A write with side effects must be idempotent, deduplicated, or not retried blindly. + +Select the recipe from those facts. Do not begin with the most expressive +schedule and tune it down after the fact. + +##### Why it matters + +Retry policy is part of the load placed on the dependency. A policy that is harmless for one request can become an outage amplifier when many callers fail at the same time. The important production questions are: + +- How soon is another attempt useful? +- How many attempts can the caller justify? +- How long may the workflow remain invisible to the user or caller? +- Would repeating the call duplicate a side effect? +- Will many callers retry on the same schedule? + +##### Core idea + +Choose the smallest schedule that describes the operational promise: + +- Use `Schedule.spaced` when each retry should wait a fixed duration after the previous attempt completes. +- Use `Schedule.fixed` only when retries must align to interval boundaries. If work runs longer than the interval, the next run happens immediately and missed intervals do not pile up. +- Use `Schedule.exponential` when repeated failures should wait progressively longer. It starts at the base delay and multiplies by the factor on each recurrence, defaulting to `2`. +- Use `Schedule.jittered` when many callers may retry together. It adjusts each delay between `80%` and `120%` of the computed delay. +- Use `Schedule.recurs` or `Schedule.take` to bound retry decisions. +- Use `Schedule.during` to bound total elapsed schedule time. +- Use `Schedule.both` when two constraints must both hold, such as backoff and a maximum retry count. The combined schedule continues only while both sides continue and uses the larger delay. + +For most flaky remote calls, the default selection is exponential backoff, jitter, and a small retry limit. Use plain fixed spacing only when the failure is known to clear quickly and the dependency can tolerate the repeated load. + +##### Practical guidance + +Use fixed delay when the call is cheap, the expected recovery window is short, and retrying does not increase pressure on an already stressed dependency. Examples include a local service startup race or a read-after-create consistency gap where a few attempts are enough. + +Use backoff when failure may mean the dependency is slow, saturated, restarting, rate limiting, or temporarily unavailable. Backoff gives the dependency more room after each failure. Prefer a conservative base delay for external systems, then cap the behavior with a retry count or elapsed budget. + +Add jitter when retries can synchronize. This includes HTTP clients in a fleet, background workers reading the same queue, scheduled jobs, or anything triggered by a shared deploy, outage, or clock boundary. Jitter is especially important with backoff because identical callers otherwise keep retrying in waves. + +Always add a limit unless the retry belongs to a deliberately supervised +background loop. For request-response work, the limit is usually a small retry +count, an elapsed-time budget, or both. Count limits make worst-case attempts +obvious; elapsed limits make caller-visible waiting time obvious. + +Check side effects before retrying writes. Retrying a `GET` or a status fetch is usually different from retrying a charge, email send, file mutation, or external workflow transition. For writes, require idempotency keys, deduplication, or a separate recovery design before selecting a retry schedule. + +If the policy cannot be explained in one sentence, split the decision: +classify retryable errors, choose the delay shape, add jitter if callers can +synchronize, then add limits so the final behavior is bounded and reviewable. + +#### 36.2 “I need to poll until something finishes” + +Use this recipe family when each repeat is a fresh observation of external +state, such as a job status endpoint, import pipeline, payment settlement, or +deployment rollout. + +The schedule should answer three questions before code is written: + +- How often may this process be observed? +- Which observed states stop polling? +- What budget prevents waiting forever? + +##### What this section is about + +Polling is not retrying a failed call. A retry schedule reacts to failures. A +polling schedule usually repeats successful reads until the read result says the +remote process is finished, failed, canceled, expired, or no longer worth +watching. + +Choose a polling recipe when the repeated operation is a status check, not the +original work request. Submit the work once. Then repeat the observation effect +with a cadence and stop condition that match the downstream system. + +##### Why it matters + +Unbounded polling creates load and hides stuck workflows. Overly aggressive +polling can turn a harmless status page into a rate-limit problem. Overly slow +polling makes users wait after the remote work has completed. + +A good polling policy is explicit about cadence, terminal states, and the +maximum time or attempts the caller is prepared to spend. + +##### Core idea + +Start with a steady cadence unless the remote system asks for something else. + +Use `Schedule.spaced(duration)` when every poll should wait for `duration` after +the previous status check completes. This is usually the safest default for +status endpoints because slow checks naturally reduce the polling rate. + +Use `Schedule.fixed(duration)` when the observation should align to a regular interval. According to `Schedule.fixed`, if the action takes longer than the interval, the next run happens immediately, but missed runs do not pile up. That is useful for clock-like monitoring, but it can be too aggressive for ordinary job-status polling. + +Use `Schedule.exponential(base)` when early results are likely to be unready and +later polls should back off. This fits long-running external workflows better +than a fast constant loop. + +Add a hard budget with `Schedule.during(duration)`, `Schedule.recurs(times)`, or +`Schedule.take(n)`. The repeated effect should stop when it sees a terminal +state; the schedule should stop when the budget is exhausted. `Schedule.during` +is natural for user-facing timeouts. `Schedule.recurs` or `Schedule.take` is +useful when the downstream service documents an attempt limit. + +Add `Schedule.jittered` when many fibers, processes, or hosts may start polling at the same time. In `Schedule`, jitter adjusts delays to a random value between 80% and 120% of the original delay, which helps avoid synchronized bursts. + +##### Practical guidance + +Classify the states first. A polling loop needs at least three categories: + +- Continue: states such as `queued`, `running`, `pending`, or `processing`. +- Success: states such as `completed`, `succeeded`, or `available`. +- Failure: states such as `failed`, `canceled`, `expired`, `rejected`, or `not_found` when disappearance is terminal for this workflow. + +Do not let the schedule be the only stop condition. The repeated effect should +interpret terminal states and stop with the appropriate success or failure. The +schedule controls when another observation is allowed; the domain result +controls whether another observation is meaningful. + +Prefer `Schedule.spaced` for most polling. It gives the remote service breathing room because the delay starts after the status call finishes. Prefer `Schedule.fixed` only when the polling contract is truly interval-based. + +Budget user-facing polling in wall-clock time, not just count. A policy such as +"poll every second for up to 30 seconds" is easier to defend than "try 30 +times" when each status call can have variable latency. For background +workflows, combine elapsed and count budgets when both service cost and total +wait matter. + +Escalate the cadence instead of polling forever. A common shape is quick initial polling for a short period, followed by slower polling, or a transition to a background notification path. Use `Schedule.andThen` when the policy has distinct phases. + +Select a different recipe when the repeated action is not a status check: + +- If the original request failed and should be attempted again, use the flaky-call retry recipe. +- If the process should run forever as service maintenance, use the periodic background loop recipe. +- If the main concern is protecting a dependency from aggregate pressure, use the overload recipe. +- If the question is only how to cap an existing schedule, use the reasonable-limit recipe. + +#### 36.3 “I need a periodic background loop” + +Choose this path for successful background work that should run again and again: +health checks, cache refreshes, local metric flushes, maintenance sweeps, +reconciliation passes, or heartbeats. + +##### What this section is about + +Before writing the worker, answer these questions: + +- Should the loop wait after each completed run, or try to stay aligned to a + regular cadence? +- Is the loop allowed to run for the whole process lifetime, or should it stop + after a count, a window, or a domain condition? +- What happens when the loop is interrupted during sleep or during the work + itself? +- How will operators see that it is running, falling behind, or stopping? + +##### Why it matters + +Background loops are easy to make unbounded by accident. `Schedule.spaced` and +`Schedule.fixed` both recur continuously unless a stopping rule or owning +lifecycle interrupts the repeated effect. + +That is often exactly what a service worker needs, but the choice should be +visible. The schedule should tell a reader whether the loop is quiet between +runs, whether it catches up after slow work, which limits apply, and where +observability is attached. + +##### Core idea + +Start with the cadence. + +Use `Schedule.spaced(duration)` when the requirement is "wait this long after a +successful run completes." This is the default for most background loops because +slow work naturally pushes the next start later. A cache refresh that takes +three seconds and repeats with `Schedule.spaced("30 seconds")` starts the next +refresh about thirty seconds after the previous refresh completes. + +Use `Schedule.fixed(duration)` when the requirement is "stay on this interval." +`fixed` keeps a regular interval and, if the action takes longer than the +interval, the next run happens immediately without building a backlog of missed +runs. That fits probes or ticks where cadence alignment matters more than quiet +time after completion. + +Then decide whether the loop is truly lifetime-bound. If it is not, add a +limit: + +- Use `Schedule.recurs(n)` when the policy is "allow at most n scheduled + recurrences after the first run." +- Use `Schedule.take(n)` when you are limiting the outputs taken from another + schedule. +- Use `Schedule.during(duration)` when the loop should continue only during an + elapsed schedule window. + +Combine an interval and a limit only when both rules are real requirements. A +bounded maintenance loop might run every thirty seconds until twenty +recurrences or fifteen minutes have been spent. A process-lifetime heartbeat may +deliberately have no schedule limit, but then cancellation must come from the +owning fiber, scope, or supervisor. + +##### Practical guidance + +Pick `spaced` unless you can explain why fixed cadence matters. `spaced` is +usually easier to reason about because each run completes before the quiet +period begins. Pick `fixed` for clock-like periodic work, and remember that it +does not launch concurrent catch-up executions by itself. + +Keep failure handling separate from periodic repetition. `Effect.repeat` uses a +schedule after successful iterations. If a flush, poll, or refresh should retry +on failure, put a short retry policy around that one iteration, then repeat the +recovered operation on the background cadence. + +Add jitter when many instances run the same loop against shared infrastructure. +For periodic loops, jitter normally belongs on the repeat schedule so ordinary +successful traffic is spread out. Keep the base interval understandable first; +then apply `Schedule.jittered` to reduce synchronized wakeups. + +Make cancellation an explicit ownership decision. A `Schedule` decides whether +and when the next recurrence should happen; it is not the worker's lifecycle. +Run long-lived loops in a scope, fiber set, layer, or supervisor that can +interrupt them during shutdown. If the loop has a natural end, express that in +the schedule or in the repeated effect's result instead of relying on process +exit. + +Attach observability to the schedule when the recurrence policy is what you +need to see. `Schedule.tapOutput` can record recurrence counts or delay outputs. +`Schedule.tapInput` observes the values supplied to the schedule: successful +values for repeats and failures for retries. For predicate-based decisions, +`Schedule.while` receives metadata such as attempt, elapsed time, output, and +computed duration. + +Prefer a small named schedule over inline composition. Names such as +`refreshEveryMinute`, `boundedStartupWarmup`, or `jitteredMetricsFlush` make the +operational promise visible at the call site. + +The common selections are: + +- Periodic worker with quiet time after each run: `Schedule.spaced`. +- Clock-like tick that should keep a regular interval: `Schedule.fixed`. +- Temporary maintenance loop: `spaced` or `fixed` plus `recurs`, `take`, or + `during`. +- Fleet-wide periodic export or refresh: base cadence plus `jittered`. +- Lifetime worker: unbounded cadence plus explicit fiber or scope ownership. + +#### 36.4 “I need to avoid overload” + +Avoiding overload is a selection problem before it is a scheduling problem: +choose recurrence that keeps aggregate traffic within what the dependency can +absorb. + +##### What this section is about + +Use this entry when the main risk is extra pressure on a shared resource: +database reconnects, HTTP retries against a struggling service, queue +redelivery, webhook delivery, cache refreshes, background polling, or +maintenance workers. + +The useful question is: "what is the maximum load this policy can add while +things are already unhealthy?" Answer that before choosing the combinators. + +##### Why it matters + +Retry and repeat policies can multiply traffic. A single fast retry loop may be +harmless in isolation, but many clients running the same loop can synchronize +into a large burst. A background worker that polls too quickly can compete with +foreground traffic. A retry policy without a budget can keep a dependency hot +long after the original work stopped being useful. + +`Schedule` makes the recurrence policy visible, but visibility only helps when +the chosen shape matches the operational risk. + +##### Core idea + +Start conservative and add pressure only when you can justify it. + +| Need | Prefer | Why | +| ---------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Keep a steady gap between attempts | `Schedule.spaced` | It waits the configured duration after each recurrence. This is usually safer than a tight loop for polling, workers, and maintenance tasks. | +| Run on interval boundaries | `Schedule.fixed` | It aligns to a regular interval and does not pile up missed runs. If work runs behind, the next delay can become zero, so it is not the default overload-avoidance choice. | +| Slow down after repeated failure | `Schedule.exponential` | It starts at the base duration and grows by the factor, defaulting to `2`. It does not stop by itself. | +| Build a custom increasing delay | `Schedule.unfold` plus `Schedule.addDelay` | Use this when the delay curve is domain-specific and should be named or explained. | +| Desynchronize many clients | `Schedule.jittered` | It adjusts each delay between 80% and 120% of the computed delay. | +| Limit the number of recurrences | `Schedule.recurs` or `Schedule.take` | Use count limits when every additional recurrence adds meaningful load or cost. | +| Limit total elapsed time | `Schedule.during` | Use time budgets when the work stops being useful after a deadline. | +| Require multiple guardrails | `Schedule.both` | It continues only while both schedules continue and uses the larger delay. This is the usual way to combine backoff with count and time limits. | + +##### Practical guidance + +Choose the schedule by the overload mechanism. + +If the problem is a tight loop, add spacing first. `Schedule.spaced` is the +plainest answer when every recurrence should leave breathing room after the +previous run. Prefer it for worker loops, polling, refreshes, and maintenance +jobs where steadiness matters more than immediate recovery. + +If the problem is retry pressure against an unhealthy dependency, use backoff. +`Schedule.exponential` is the standard starting point because later failures +become less aggressive. Pick a base delay that would still be acceptable if +every caller used it at the same time. + +If the problem is synchronized clients, add jitter. `Schedule.jittered` changes +the delay range, not the stop condition. Apply it to spread retries across the +fleet, then decide whether a strict maximum delay is required. + +If the problem is unbounded tail behavior, cap and budget the policy. Use a +maximum delay, typically with `Schedule.modifyDelay`, when operators need to +know the longest single wait. Use `Schedule.recurs` or `Schedule.take` when the +number of extra attempts matters. Use `Schedule.during` when total elapsed time +matters more than exact count. + +If the problem is mixed failure modes, classify before retrying. Timeouts, +temporary unavailability, and rate-limit responses may deserve conservative +retry. Validation errors, authorization failures, permanent configuration +errors, and unsafe non-idempotent side effects usually should not enter the +retry schedule at all. + +##### Selection checklist + +Before choosing the recipe, answer these questions: + +- What shared resource is protected: a service, database, queue, provider quota, + CPU pool, or user-facing path? +- Is the operation safe to retry, or does it need idempotency first? +- Should the first retry be delayed, or is one quick retry acceptable? +- Should later attempts become slower with `Schedule.exponential`? +- Will many clients run the same policy, requiring `Schedule.jittered`? +- What is the largest acceptable single delay after jitter and any cap? +- What is the maximum number of extra attempts? +- What is the maximum elapsed budget? +- Which error or result classes must stop immediately? + +##### Common selections + +For a fragile downstream service, choose exponential backoff, jitter, a maximum +delay, a retry count, an elapsed budget, and a retryable-error classifier. This +is the safest general-purpose overload shape. + +For low-risk background polling, choose `Schedule.spaced` with a count or +elapsed budget if the work should eventually stop. Add jitter only when many +instances poll the same dependency. + +For provider quotas or rate limits, choose slower spacing or backoff and treat +rate-limit responses as their own class. Do not handle them like ordinary +network glitches if the provider is explicitly asking the client to slow down. + +For user-facing workflows, keep the budget short. Avoid making a person wait +through a background-worker retry policy. Prefer a small number of attempts and +a clear failure path over long invisible retrying. + +For fleet-wide recovery after an incident, favor larger base delays, jitter, and +strict limits. The aggregate behavior matters more than one process recovering +as quickly as possible. + +##### Notes and caveats + +`Schedule.exponential`, `Schedule.spaced`, `Schedule.fixed`, and +`Schedule.jittered` do not impose a useful operational limit by themselves. +Pair them with `Schedule.recurs`, `Schedule.take`, `Schedule.during`, or an +input-aware stop condition. + +`Schedule.jittered` in Effect uses an 80%-120% range. If the maximum delay must +be strict, cap after jitter with `Schedule.modifyDelay` instead of assuming the +base backoff cap remains exact. + +`Schedule.both` has intersection semantics: it continues only while both sides +continue and chooses the larger delay. That is usually what overload protection +wants. A composition that continues while either side continues can extend +traffic longer than intended. + +Client-side scheduling reduces retry pressure, but it is not a replacement for +server-side rate limits, queues, backpressure, circuit breakers, or load +shedding. + +#### 36.5 “I need to stop after a reasonable limit” + +Use this entry when a retry, repeat, poll, or background loop needs a clear +stopping boundary. + +##### What this section is about + +This entry is about selecting the first guardrail, not tuning the delay curve. +Ask what makes the next recurrence unreasonable: + +- Too many attempts: use `Schedule.recurs`. +- Too many outputs from an existing schedule: use `Schedule.take`. +- Too much elapsed time: use `Schedule.during`. +- A schedule output says the work has reached a boundary: use an output predicate with `Schedule.while`. + +That decision should be made before adding backoff, spacing, jitter, or logging. + +##### Why it matters + +An unbounded schedule is easy to write and hard to defend. A retry that can +continue forever can hide a failing dependency. A poller without a time budget +can make a user-facing workflow hang. A background loop without a count, time, +or output boundary can turn a temporary condition into persistent load. + +Reasonable limits also make reviews easier. The reader should be able to tell +whether the policy stops because it exhausted attempts, exceeded a wall-clock +budget, consumed enough outputs, or observed a domain-specific output. + +##### Core idea + +Use the limit that matches the thing you are protecting. + +Use `Schedule.recurs` when the policy is "try again up to this many times." In +`Schedule.ts`, `recurs(times)` can only be stepped the specified number of times +before it terminates, and it outputs the recurrence count. This is the clearest +choice for retry ceilings such as "at most three retries." + +Use `Schedule.take` when you already have a schedule shape and want to cap how +many outputs it may produce. This is usually the right fit for limiting +`Schedule.spaced`, `Schedule.fixed`, `Schedule.exponential`, +`Schedule.fibonacci`, or another composed schedule without changing its cadence +semantics. + +Use `Schedule.during` when the defensible boundary is elapsed time. +`during(duration)` recurs only while elapsed duration remains within the +supplied duration. It is the right primitive for "retry for up to 30 seconds", +"poll during startup", or "keep sampling during a short diagnostic window." + +Use an output predicate when the schedule output carries the boundary. The +exported predicate combinator is `Schedule.while`, whose predicate receives +metadata including `output`, `attempt`, `elapsed`, and `duration`. Reach for +this when the output has meaning, such as a counter, accumulated value, +state-machine state, or measured delay, and stopping depends on that value +rather than on a fixed count or clock budget. + +##### Practical guidance + +Prefer one primary stop condition and add a second only when it protects a +different failure mode. A retry policy might use increasing delays and still cap +attempts with `Schedule.recurs`; a poller might use cadence plus +`Schedule.during` so it cannot wait forever. + +Do not treat `Schedule.recurs` and `Schedule.take` as interchangeable names for +the same idea. `recurs` is itself the count-based schedule. `take` limits +another schedule after you have chosen its cadence or output behavior. + +When the limit is operational, make it visible in the recipe name or surrounding +code: attempts, outputs, elapsed time, or output predicate. If nobody can say +which one stopped the schedule, the policy is too hard to operate. + +### 37. Decision Matrix by Problem Shape + +#### 37.1 Transient failure vs permanent failure + +Use this entry when the first decision is whether a failure should be retried at +all. Delay choice comes after classification. + +`Schedule` describes recurrence; it does not decide which domain errors are +retryable. In `Effect.retry`, the typed failure is the schedule input, so keep +classification close to the retry policy. A transient failure may succeed later +without changing the request. A permanent failure will not be repaired by +waiting. + +##### Decision matrix + +| Failure shape | Retry choice | Schedule shape | Why | +| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Temporary network interruption, timeout, connection reset, stale leader, or overloaded dependency | Retry | `Schedule.exponential` plus `Schedule.recurs` or `Schedule.during`; add `Schedule.jittered` when many clients may retry together | The request may succeed after the dependency recovers. Increasing delay avoids turning a temporary problem into a retry storm. | +| Rate limit or explicit "try again later" response | Retry, but slow down | Prefer a delay derived from the response when available; otherwise use bounded `Schedule.exponential`, often with `Schedule.jittered` | The downstream service is telling the caller to reduce pressure. A tight fixed retry ignores that signal. | +| Conflict, lock contention, compare-and-set race, or eventually consistent read | Retry briefly | `Schedule.spaced` or a small `Schedule.exponential`, bounded by `Schedule.recurs` or `Schedule.during` | The retry is local to a race window. Keep it small so real logical conflicts surface quickly. | +| Validation error, malformed input, missing required data, unsupported operation, or invariant violation | Do not retry | No retry schedule, or stop with `Schedule.while` when a shared schedule handles mixed errors | Waiting cannot repair the request. Retrying only delays the useful error. | +| Authentication or authorization failure | Usually do not retry | No retry schedule unless the workflow includes an explicit credential refresh step before retrying | A schedule alone cannot make invalid credentials valid. Classify refreshed credentials separately from permanent denial. | +| Not found | Depends on the domain | Retry only when the object is expected to appear; otherwise fail immediately | "Not found" can mean eventual consistency, delayed provisioning, or a wrong identifier. The schedule follows the domain meaning, not the status code alone. | +| Unknown or mixed failure | Retry conservatively only if the operation is safe to repeat | A small `Schedule.recurs` or short `Schedule.during` budget, often with `Schedule.exponential` | Ambiguous errors should not receive an unbounded policy. Use the smallest retry budget you can defend operationally. | + +##### How classification changes the schedule + +For transient failures, the schedule answers: how long are we willing to wait +for the external condition to change? A typical shape is `Schedule.exponential` +for increasing delay, `Schedule.recurs` or `Schedule.take` for a hard recurrence +limit, and sometimes `Schedule.during` for an elapsed budget. Add +`Schedule.jittered` when many fibers, processes, or hosts can fail together. + +For permanent failures, the schedule should stop immediately. If one retry +policy receives both transient and permanent errors, guard it with +`Schedule.while` and inspect `metadata.input`. Continue only while the failure +is classified as retryable. + +For uncertain failures, do not treat uncertainty as transience. Use a small +bounded retry only when repeating the operation is safe, then surface the final +error. If the domain later learns how to distinguish permanent from transient +cases, narrow the retry predicate instead of expanding the delay policy. + +##### Selection rules + +Start with classification before timing: + +1. If the request is wrong, do not retry. +2. If the dependency or timing window may recover, retry with backoff. +3. If the failure can happen across many clients, add jitter. +4. If the operation can duplicate side effects, require idempotency before retrying. +5. If the classifier is incomplete, keep the retry budget short. + +Then choose the smallest schedule that expresses the decision. `Schedule.spaced` +and `Schedule.fixed` express steady cadence. `Schedule.exponential` expresses a +delay that grows after each failed attempt. `Schedule.recurs`, `Schedule.take`, +and `Schedule.during` express hard bounds. `Schedule.both` uses the larger delay +from its two inputs and stops when either input schedule stops. + +#### 37.2 Immediate responsiveness vs infrastructure safety + +Fast retries and tight polling can reduce visible latency, but they also add +load when a dependency may already be slow, unavailable, or recovering. Choose +the recurrence shape from the scarce resource: caller patience or downstream +capacity. + +This entry is a selection aid, not a new primitive. + +##### Decision matrix + +| Problem shape | Prefer | Schedule shape | Why | +| ---------------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| A user is actively waiting and the operation is cheap, local, or already rate-limited upstream | Fast retries or short polling | `Schedule.recurs` or `Schedule.take` with a very small `Schedule.spaced` delay, usually under a short `Schedule.during` budget | The main cost is user-visible latency. A few quick attempts can hide brief races without creating a long invisible wait. | +| The dependency may be overloaded, restarting, or shared by many callers | Safer spacing or backoff | `Schedule.exponential` with a count or elapsed limit, often with `Schedule.jittered` | Increasing delay gives the dependency recovery time. Jitter reduces synchronized retry waves across a fleet. | +| The workflow polls for readiness after creating work, such as a job or cache entry | Start responsive, then slow down | A short initial policy followed by `Schedule.exponential`, `Schedule.fibonacci`, or a larger `Schedule.spaced` cadence | Early success is common, but persistent absence should not become high-frequency background load. | +| The work is infrastructure maintenance, heartbeats, or health checks | Stable cadence with explicit bounds | `Schedule.fixed` for interval boundaries or `Schedule.spaced` for a delay after each run | `fixed` stays near clock-like boundaries without replaying missed runs. `spaced` waits after each completed run. | +| The operation has side effects or can duplicate external work | Infrastructure safety first | Backoff plus a low `Schedule.recurs` count, and only after idempotency is established | Fast retries can duplicate writes, messages, or charges. The schedule cannot make an unsafe operation safe. | +| The path is high fan-out, batch-oriented, or run by many service instances | Conservative spacing, jitter, and budgets | `Schedule.exponential` or `Schedule.spaced`, bounded with `Schedule.recurs`, `Schedule.take`, or `Schedule.during`; add `Schedule.jittered` when callers may align | Aggregate load matters more than one caller's latency. A harmless-looking 100 millisecond retry can become expensive when multiplied. | + +##### Selection rule + +Choose fast recurrence only when all of these are true: the operation is cheap, +the recurrence count is low, the caller is waiting, duplicate effects are +acceptable or impossible, and the dependency is not already under pressure. + +Choose safer spacing or backoff when any of these are true: many callers may +retry together, the dependency is shared, the failure mode may be overload, the +operation has meaningful side effects, or the workflow can continue +asynchronously. + +##### Practical guidance + +Treat responsiveness as a budget, not a default. A fast policy should normally +have both a small recurrence limit and a short elapsed-time limit. After that +budget is spent, fail visibly, switch to slower polling, or move the work to the +background. + +Treat infrastructure safety as the default for shared systems. Use +`Schedule.exponential` when repeated failure should slow the caller down, +`Schedule.spaced` when each recurrence should wait after the previous run, +`Schedule.fixed` when runs should target regular interval boundaries, and +`Schedule.jittered` when many schedules may start together. + +#### 37.3 Fixed cadence vs adaptive cadence + +Use this matrix when the main question is whether work should run on a +predictable cadence or slow down in response to repeated failure, contention, or +uncertainty. `Schedule.fixed` and `Schedule.spaced` express steady cadence. +`Schedule.exponential`, `Schedule.fibonacci`, `Schedule.modifyDelay`, and +`Schedule.jittered` express adaptive behavior. + +##### Decision matrix + +| Problem shape | Prefer | Why | Guardrails | +| ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Health checks, metric flushes, cache refreshes, or maintenance work that should stay aligned to a regular interval | `Schedule.fixed(interval)` | It targets fixed interval boundaries. If work overruns the interval, the next recurrence can be immediate, but missed runs are not replayed. | Add `Schedule.jittered` when many instances start together. Add `Schedule.take`, `Schedule.recurs`, or `Schedule.during` when the cadence is temporary. | +| Worker loops where each completed item should be followed by a pause | `Schedule.spaced(duration)` | It waits after each action completes. Long-running work naturally pushes the next run later. | Use this for politeness and load smoothing. Add count or elapsed limits for bounded workflows. | +| Retrying transient failures against a dependency that may be overloaded | `Schedule.exponential(base, factor)` or `Schedule.fibonacci(base)` | The delay grows as failures continue, reducing pressure on the dependency. | Combine with `Schedule.recurs`, `Schedule.take`, or `Schedule.during`. Add `Schedule.jittered` for fleet-wide retries. | +| Retrying rate limits, quota responses, or service-specific overload signals | Adaptive backoff with `Schedule.modifyDelay` or a stateful schedule | The next delay should reflect the service signal, not only a local interval. | Respect server-provided retry hints when available. Cap the maximum delay if user experience or job latency matters. | +| Polling a known external state transition | Start with `Schedule.spaced(duration)` | Polling is easier to reason about when every completed observation is followed by the same pause. | Switch to adaptive polling only if early responsiveness matters or later polling should become less frequent. Stop on terminal status. | +| Reconnecting clients, brokers, sockets, or control-plane calls after failure | Backoff plus jitter | Fast repeated reconnects can amplify an incident. Adaptive delay gives the remote system room to recover. | Add a maximum delay, a maximum attempt count, and jitter for many clients. | + +##### Fixed cadence choices + +`Schedule.fixed` and `Schedule.spaced` are both fixed-delay tools, but they +answer different operational questions. + +- Choose `Schedule.fixed` when the desired shape is "run on this clock-like + interval." It fits recurring work that should remain aligned to interval + boundaries. +- Choose `Schedule.spaced` when the desired shape is "after each completion, + wait this long." It fits work where action duration should push the next + recurrence later. + +The overrun behavior is the key difference. With `fixed`, slow work can be +followed by an immediate recurrence, while skipped intervals are not replayed. +With `spaced`, the delay is applied after the action completes, so overruns slow +the cadence automatically. + +##### Adaptive cadence choices + +Adaptive cadence is the better default when each repeated failure is evidence +that the next attempt should be more conservative. `Schedule.exponential` grows +by multiplying a base delay by the configured factor for each recurrence. +`Schedule.fibonacci` grows more gradually. `Schedule.modifyDelay` can clamp or +otherwise change the computed delay. `Schedule.jittered` randomizes each delay +between 80% and 120% of the current delay to reduce synchronization. + +Use adaptive policies for retries more often than for ordinary periodic work. A +fixed cadence says, "this work is expected and routine." Backoff says, "the +system is failing or unavailable, so each new attempt should be less +aggressive." + +##### Selection rules + +- If the work is routine and expected to succeed, start with `Schedule.fixed` or `Schedule.spaced`. +- If the repeated action is a retry after failure, start with backoff unless the dependency is local, cheap, and known to recover quickly. +- If action duration should not shift the intended wall-clock cadence, use `Schedule.fixed`. +- If action duration should naturally slow the loop, use `Schedule.spaced`. +- If many clients can execute the same schedule at the same time, add jitter before relying on either fixed or adaptive timing. +- If the workflow has user-visible latency, add explicit attempt or elapsed-time limits instead of allowing long adaptive tails to grow invisibly. + +##### Common mistakes + +- Using `Schedule.fixed` for slow work and being surprised by immediate follow-up runs after overruns. +- Using `Schedule.spaced` when operators expect a wall-clock cadence such as "every minute." +- Using a fixed retry delay against an overloaded remote service, which can keep pressure constant when pressure should decrease. +- Adding exponential backoff to routine polling without a reason, which can make normal progress look sluggish. +- Forgetting jitter in a fleet, where identical fixed or adaptive policies can synchronize across instances. + +#### 37.4 User-facing workflow vs background process + +Use this entry to choose retry and polling budgets for visible workflows and +unattended processes. The split is not "frontend versus backend"; it is whether +a person or request path is waiting, or whether a supervised process can keep +working after the caller has moved on. + +##### Decision matrix + +| Decision | User-facing workflow | Background process | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Primary budget | Human or request latency. Prefer a short `Schedule.during` budget, a small `Schedule.recurs` count, or both. | Freshness, recovery, quota, or operational pressure. Use a larger elapsed budget only when delayed success is still valuable. | +| Retry aggressiveness | Start small and stop quickly. A brief `Schedule.exponential` policy can smooth transient failures, but long invisible waiting is worse than a clear failure. | Usually less aggressive per attempt. Start with a larger base delay, cap growth with `Schedule.modifyDelay` when needed, and avoid keeping failed work alive indefinitely. | +| Polling cadence | Use a responsive cadence only while the user is plausibly waiting. `Schedule.spaced` is clear when each poll should wait after the previous check completes. | Prefer steady, predictable cadence. `Schedule.spaced` means "run, then wait"; `Schedule.fixed` means "stay near this interval boundary" and may run immediately after slow work without replaying missed runs. | +| Composition style | Use `Schedule.both` for strict limits: continue only while both the cadence and the budget continue. This stops when either the count or elapsed budget is exhausted. | Use `Schedule.both` for local limits too, but review aggregate load separately. Use `Schedule.either` only when extending the policy is intentional, because it continues while either side continues and uses the smaller delay. | +| Jitter | Optional for a single visible workflow; useful when many clients can retry or poll together. Jitter may make one UI less predictable. | Usually preferred for fleets. `Schedule.jittered` spreads each delay between 80% and 120% of the incoming delay, reducing synchronized pressure. | +| Failure surface | Return the typed failure, timeout, or still-pending result promptly so the caller can choose the next product action. | Emit logs, metrics, or alerts when the process exhausts its retry budget. Supervision may restart the process, but the schedule should not hide repeated failure. | +| Idempotency requirement | High for writes and workflow steps. Retrying a broad user action can duplicate side effects if only one inner step was transient. | Still required. Background execution does not make unsafe side effects safe; it only changes how much time the system can spend recovering. | + +##### Selection rules + +Choose the policy from the thing that is scarce. + +If caller patience is scarce, use a short elapsed budget and a small retry +count. `Schedule.exponential` can start with tens or hundreds of milliseconds, +but it should usually be paired with `Schedule.recurs` or `Schedule.during` +through `Schedule.both`. The result stops as soon as either the recurrence count +or time window is spent. + +If dependency capacity is scarce, reduce retry pressure. Use a slower base +delay, increasing backoff, jitter for many callers, and explicit limits. A +background worker can wait longer than a request, but it still needs a reason to +keep trying and a visible exhaustion point. + +If freshness is scarce, choose cadence before retry behavior. For a status view +or progress check, poll quickly only inside a short window. For cache refresh, +reconciliation, or maintenance, prefer a clear `Schedule.spaced` cadence and +keep failure retry inside one iteration so "recover this run" and "run again +later" remain separate decisions. + +If synchronization is the risk, add jitter to the recurrence delay. This matters +more for background workers, service replicas, browser clients released at the +same time, and control-plane polling than for a single local workflow. + +##### Reading the schedule + +Review the final policy by asking three questions: + +| Question | What to look for | +| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| How often can it create load? | `Schedule.spaced`, `Schedule.fixed`, `Schedule.exponential`, or a custom delay. | +| When does it stop? | `Schedule.recurs`, `Schedule.take`, `Schedule.during`, or an input-aware condition. | +| Does composition tighten or extend it? | `Schedule.both` tightens by requiring both sides to continue and using the larger delay. `Schedule.either` extends by allowing either side to continue and using the smaller delay. | + +For user-facing work, the answers should be small and easy to explain in product +terms. For background work, the answers should be easy to explain in operational +terms: expected load, maximum recovery window, fleet behavior, and what happens +when the budget is exhausted. + +#### 37.5 Single-instance behavior vs fleet-wide behavior + +Use this entry when a locally reasonable schedule may multiply across many +processes, pods, browsers, or workers. Schedules do not coordinate across +instances by themselves: each fiber or process steps its own schedule and sleeps +for its own computed delay. + +Compare the local recurrence to the aggregate behavior it creates. +`Schedule.spaced("10 seconds")` is modest for one process. Across 200 aligned +instances, it can mean up to 200 follow-up attempts every interval. +`Schedule.exponential`, `Schedule.recurs`, `Schedule.take`, `Schedule.during`, +and `Schedule.jittered` control different parts of that multiplication. + +##### Decision matrix + +| Problem shape | Main question | Prefer | Why | +| -------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| One process performs a local background loop | How often should this one loop run? | `Schedule.spaced` for a gap after work, or `Schedule.fixed` for a clock-like interval | The policy is mostly local cadence. `fixed` keeps a regular interval without replaying missed runs. `spaced` waits after each recurrence decision. | +| One process retries a transient failure | How quickly should this caller recover, and when should it stop? | `Schedule.exponential` with `Schedule.recurs`, `Schedule.take`, or `Schedule.during` | Backoff reduces repeated pressure from the same caller. Count and elapsed-time limits keep the retry from becoming an invisible long-running workflow. | +| Many instances may retry the same dependency | What is the aggregate retry rate? | Backoff plus a retry limit, usually with `Schedule.jittered` | Instance count multiplies attempts. A policy that allows 5 retries allows up to `instances * 5` retries for a shared outage. Jitter spreads those decisions instead of letting them happen in lockstep. | +| Many instances poll the same control plane | What is the steady-state request rate? | Wider `Schedule.spaced` or `Schedule.fixed` intervals, often jittered | Periodic work multiplies continuously, not only during failures. A 30-second poll from 120 instances is roughly 4 requests per second before retries. | +| Many instances start at the same time | What happens after deploys, restarts, or autoscaling? | Add jitter to the runtime cadence, and avoid very short initial spacing unless the dependency can absorb it | Identical schedules started together tend to stay aligned. Jitter adjusts each delay between 80% and 120% of the original delay, breaking alignment without changing the base policy beyond recognition. | +| Downstream capacity is strict | Is local scheduling enough? | Schedule caps plus external rate limiting, leasing, partitioning, or queue backpressure | `Schedule` controls when one workflow tries again. It does not enforce a fleet-wide quota, global concurrency limit, or single active owner. Use coordination when the invariant is global. | + +##### Practical guidance + +Start by estimating the local policy, then multiply it by the number of +instances that can run it at once. Include the first attempt outside the +schedule when reasoning about retries: the schedule governs follow-up +decisions, while the original operation has already happened. + +Use spacing when the main problem is steady-state load. `Schedule.spaced` is +easy to reason about for work that should leave a gap after completion. +`Schedule.fixed` is useful when the interval should remain tied to regular time +windows; late work may run the next recurrence immediately, but missed runs do +not accumulate. + +Use caps when the main problem is worst-case retry volume. `Schedule.recurs(5)` +or `Schedule.take(5)` may be small locally, but the fleet-wide maximum is still +multiplied by active instances and failing operations per instance. Add +`Schedule.during` when the elapsed budget matters more than the exact count. + +Use jitter when the main problem is synchronization. `Schedule.jittered` +randomly modifies each delay between 80% and 120% of the computed delay. It does +not reduce the total number of attempts; it spreads them over time. That makes +it valuable for fleets and less appropriate when exact cadence is promised. + +If the desired behavior is "only one instance should do this", do not solve it +with a local schedule. Use a lease, leader election, a queue with one consumer +per partition, or another coordination primitive. Then apply `Schedule` inside +the elected or assigned worker to describe that worker's local retry, polling, +or repeat policy. + +### 38. Glossary + +#### 38.1 Retry + +Retry reruns an effect after a typed failure. In `Effect.retry`, the first +attempt runs immediately. After each failure, that failure value becomes the +input to the `Schedule`; the schedule either halts, propagating the last +failure, or continues after the delay it computes. + +"Typed failure" means a value in the effect's error channel. Defects and +interruptions are not treated as retryable failures by `Effect.retry`. + +Retry differs from repeat by the signal that advances the schedule. Retry +advances after failure and stops on success. Repeat advances after success and +stops on failure. That difference decides what the schedule can inspect, what +the surrounding effect returns, and whether the policy is recovery logic or +normal recurrence. + +Use retry when a later attempt may succeed because the failure is transient: +for example, a timeout, connection reset, temporary service unavailability, or +rate-limit response with a valid retry path. Bound retry with an attempt limit, +an elapsed-time budget, or both. Add backoff and jitter when many callers could +otherwise create synchronized extra load. + +For writes, sends, publishes, payments, provisioning, deletes, and other +externally visible side effects, only retry when duplicate execution is safe or +guarded by the operation's protocol. A schedule can time and limit retries; it +cannot make an unsafe side effect idempotent. + +#### 38.2 Repeat + +Repeat reruns an effect after it succeeds. In `Effect.repeat`, the first +execution runs immediately. Each successful value then becomes the input to the +`Schedule`; the schedule decides whether to run again, how long to wait before +that run, and which schedule output is returned when repetition stops. + +Repeat stops on failure unless the repeated effect handles or retries that +failure itself. Retry is the opposite shape: failures feed the schedule, and a +success ends the retry loop. + +Because the first execution is outside the schedule, count limits describe +additional successful recurrences. `Schedule.recurs(3)` allows three repeats +after the initial run, not three total executions. + +Use repeat for polling, heartbeats, refresh loops, sampling, maintenance loops, +and other workflows where the next run depends on the previous successful +observation. Use retry when the next run is a response to a typed failure. + +Common repeat policies start with `Schedule.spaced` for a delay after each +successful run or `Schedule.fixed` for wall-clock cadence. Add `Schedule.recurs` +or `Schedule.take` for a count budget, `Schedule.during` for an elapsed-time +budget, `Schedule.while` for a value-based stop condition, and +`Schedule.passthrough` when the final result should be the latest successful +value rather than the schedule's own counter or duration output. + +Make the stopping condition visible. An unbounded heartbeat may be deliberate, +but polling and refresh loops usually need a count limit, time budget, domain +predicate, or surrounding cancellation boundary. + +#### 38.3 Polling + +Polling repeats a successful observation until a domain condition is met or a +recurrence budget expires. The observation is the successful value produced by +the effect being repeated. The `Schedule` decides whether to observe again, how +long to wait, and when the loop has run long enough. + +Keep domain status separate from operational failure. A response such as +`"pending"`, `"running"`, or `"not ready"` is usually a successful value that +may justify another poll. A timeout, malformed response, authorization failure, +or unavailable endpoint is an effect failure unless the program handles or +retries it separately. + +A typical polling policy uses `Schedule.spaced` for a gap after each completed +check, or `Schedule.fixed` when a wall-clock cadence matters. Add +`Schedule.passthrough` when the final result should be the latest observed +status. Add `Schedule.while` to continue only while the observed status is +non-terminal. Add `Schedule.during`, `Schedule.recurs`, or `Schedule.take` for +elapsed-time or count budgets, and `Schedule.jittered` when many clients might +otherwise poll together. + +The stop condition and budget are checked at schedule decision points after +successful observations. They do not turn a failing request into a successful +poll result, and they do not interrupt a request already in flight. Use a +per-request timeout when each individual observation needs a hard deadline. + +For user-facing polling, prefer short budgets and explicit outcomes over long +invisible waiting. For background reconciliation, keep the cadence modest and +measure aggregate load across all workers, not just one loop. + +#### 38.4 Backoff + +Backoff is the delay shape used when recurrence should become less aggressive, +most often after repeated transient failures. It is not a separate Effect +primitive. It is the sequence of delays a `Schedule` offers between attempts. + +Backoff is only one part of a recurrence policy. The delay shape says when the +next attempt may start. Other combinators decide how many recurrences are +allowed, which inputs may continue, and when an elapsed-time budget has expired. + +Common delay shapes: + +- Fixed backoff uses the same delay each time. `Schedule.spaced` waits after + each completed attempt; `Schedule.fixed` targets interval boundaries and does + not pile up missed runs. +- Linear backoff increases by a constant amount on each recurrence. There is no + dedicated linear-backoff constructor in `Schedule.ts`; model this with state, + such as `Schedule.unfold`, plus `Schedule.addDelay` or + `Schedule.modifyDelay`. +- Exponential backoff multiplies the delay each time. + `Schedule.exponential(base, factor)` starts with the base duration and uses a + default factor of `2`. +- Capped backoff is an increasing delay with an upper bound. The cap limits the + delay, not the number of recurrences. + +Use fixed backoff when a steady retry pace is acceptable. Use linear backoff +when pressure should increase gently. Use exponential backoff when repeated +failure may indicate overload or temporary unavailability. Add a cap when the +tail delay would otherwise exceed the workflow's latency or recovery budget. + +Backoff should usually be paired with an explicit stop condition such as +`Schedule.recurs`, `Schedule.take`, or `Schedule.during`. Add +`Schedule.jittered` when many fibers, processes, or clients could otherwise +retry on the same delay boundaries. In `Schedule.ts`, jitter adjusts each delay +randomly between 80% and 120% of the original delay. + +#### 38.5 Idempotency + +Idempotency is the property that running the same operation more than once has +the same intended effect as running it once. For retryable writes and other side +effects, it means a repeated attempt does not create duplicate orders, duplicate +payments, duplicate messages, or any other extra externally visible change. + +The safety requirement lives outside the schedule. A `Schedule` can time and +bound a retry, but duplicate safety belongs to the operation being retried or +to the protocol around it. + +Retries deliberately repeat work after failure. With reads, duplicate execution +is often harmless. With writes, a failure may only mean the caller did not +observe the result; the remote system may already have committed the change. +Retrying that write without a duplicate guard can turn a transient timeout into +duplicated state. + +Before retrying a side effect, decide how duplicate attempts are recognized and +collapsed. Common guards include idempotency keys, deterministic request +identifiers, conditional writes, upserts keyed by stable business identity, +consumer-side de-duplication, and transactional checks that make "already done" +a successful outcome. The important property is that every retry attempt +represents the same logical operation, not a new operation with the same +payload. + +Use `Schedule.recurs`, `Schedule.take`, `Schedule.during`, backoff, and jitter +to control retry behavior, but do not treat those controls as substitutes for +idempotency. A well-timed retry policy is still unsafe if each attempt may +perform the side effect again. + +If the operation cannot be made idempotent, prefer surfacing the failure, +recording an ambiguous outcome for reconciliation, or moving the work behind a +durable queue that can enforce de-duplication. diff --git a/.repos/effect-smol/deno.json b/.repos/effect-smol/deno.json new file mode 100644 index 00000000000..216b21ee9a0 --- /dev/null +++ b/.repos/effect-smol/deno.json @@ -0,0 +1,33 @@ +{ + "$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/*/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-smol/docker-compose.yaml b/.repos/effect-smol/docker-compose.yaml new file mode 100644 index 00000000000..1a41e02f13d --- /dev/null +++ b/.repos/effect-smol/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-smol/dprint.json b/.repos/effect-smol/dprint.json new file mode 100644 index 00000000000..06014ed4838 --- /dev/null +++ b/.repos/effect-smol/dprint.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://dprint.dev/schemas/v0.json", + "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-smol/flake.lock b/.repos/effect-smol/flake.lock new file mode 100644 index 00000000000..5613a891143 --- /dev/null +++ b/.repos/effect-smol/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1778458615, + "narHash": "sha256-cY07EsdhBJ8tFXPzDYevgqxRev9ZLxFonuq9wmq5kwg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "c6e5ca3c836a5f4dd9af9f2c1fc1c38f0fac988a", + "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-smol/flake.nix b/.repos/effect-smol/flake.nix new file mode 100644 index 00000000000..1280592939e --- /dev/null +++ b/.repos/effect-smol/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_25 + python3 + ]; + }; + }); + }; +} diff --git a/.repos/effect-smol/jsdocs.config.json b/.repos/effect-smol/jsdocs.config.json new file mode 100644 index 00000000000..07ea6b99950 --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/cause.md b/.repos/effect-smol/migration/cause.md new file mode 100644 index 00000000000..0c223cdb0ee --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/equality.md b/.repos/effect-smol/migration/equality.md new file mode 100644 index 00000000000..d5e2352eac7 --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/error-handling.md b/.repos/effect-smol/migration/error-handling.md new file mode 100644 index 00000000000..93ab919eefe --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/fiber-keep-alive.md b/.repos/effect-smol/migration/fiber-keep-alive.md new file mode 100644 index 00000000000..fe8efa3c4f3 --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/fiberref.md b/.repos/effect-smol/migration/fiberref.md new file mode 100644 index 00000000000..51a08862216 --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/forking.md b/.repos/effect-smol/migration/forking.md new file mode 100644 index 00000000000..d924272bb2e --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/generators.md b/.repos/effect-smol/migration/generators.md new file mode 100644 index 00000000000..363f7158a2c --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/layer-memoization.md b/.repos/effect-smol/migration/layer-memoization.md new file mode 100644 index 00000000000..a67cc6e74d5 --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/runtime.md b/.repos/effect-smol/migration/runtime.md new file mode 100644 index 00000000000..11b64ffcc96 --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/schema.md b/.repos/effect-smol/migration/schema.md new file mode 100644 index 00000000000..da26e692bea --- /dev/null +++ b/.repos/effect-smol/migration/schema.md @@ -0,0 +1,1032 @@ +# 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 | +| `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 + +### 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) +``` + +### 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) +``` + +### 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-smol/migration/scope.md b/.repos/effect-smol/migration/scope.md new file mode 100644 index 00000000000..4dd5f1305b8 --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/services.md b/.repos/effect-smol/migration/services.md new file mode 100644 index 00000000000..9ee34d63e02 --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/v3-to-v4.md b/.repos/effect-smol/migration/v3-to-v4.md new file mode 100644 index 00000000000..5e3f3c7718d --- /dev/null +++ b/.repos/effect-smol/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-smol/migration/yieldable.md b/.repos/effect-smol/migration/yieldable.md new file mode 100644 index 00000000000..f57781328b9 --- /dev/null +++ b/.repos/effect-smol/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-smol/package.json b/.repos/effect-smol/package.json new file mode 100644 index 00000000000..d3f66b756d3 --- /dev/null +++ b/.repos/effect-smol/package.json @@ -0,0 +1,102 @@ +{ + "private": true, + "type": "module", + "packageManager": "pnpm@10.17.1", + "scripts": { + "clean": "node scripts/clean.mjs", + "codegen": "pnpm --recursive --parallel --filter \"./packages/**/*\" run codegen", + "codemod": "node scripts/codemod.mjs", + "build": "tspc -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build", + "build:tsgo": "tsgo -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build:tsgo", + "circular": "node scripts/circular.mjs", + "test": "vitest", + "coverage": "vitest --coverage", + "check": "tspc -b tsconfig.json", + "check:tsgo": "tsgo -b tsconfig.json", + "check:cookbook:schedule": "node scripts/check-cookbook-schedule.ts cookbooks/schedule.md", + "check-recursive": "pnpm --recursive --filter \"./packages/**/*\" exec tspc -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", + "changeset-version": "changeset version", + "changeset-publish": "pnpm codemod && pnpm build && changeset publish", + "prepare": "effect-tsgo patch" + }, + "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/language-service": "^0.85.1", + "@effect/oxc": "workspace:^", + "@effect/tsgo": "^0.7.2", + "@effect/utils": "workspace:^", + "@effect/vitest": "workspace:^", + "@faker-js/faker": "^10.4.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", + "@typescript/native-preview": "7.0.0-dev.20260512.1", + "@vitest/browser": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/expect": "4.1.6", + "@vitest/web-worker": "4.1.6", + "ast-types": "^0.14.2", + "babel-plugin-annotate-pure-calls": "^0.5.0", + "dprint": "^0.54.0", + "glob": "^13.0.6", + "happy-dom": "^20.9.0", + "jscodeshift": "^17.3.0", + "madge": "^8.0.0", + "oxlint": "1.42.0", + "playwright": "^1.60.0", + "rollup": "^4.60.3", + "rollup-plugin-bundle-stats": "^4.22.1", + "rollup-plugin-esbuild": "^6.2.1", + "rollup-plugin-visualizer": "^7.0.1", + "terser": "^5.47.1", + "ts-patch": "^4.0.1", + "tstyche": "^7.1.0", + "typescript": "^6.0.3", + "vite": "^7.3.2", + "vite-tsconfig-paths": "^6.1.1", + "vitest": "4.1.4", + "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-smol/packages/ai/anthropic/CHANGELOG.md b/.repos/effect-smol/packages/ai/anthropic/CHANGELOG.md new file mode 100644 index 00000000000..35c94ecb3d5 --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/CHANGELOG.md @@ -0,0 +1,539 @@ +# @effect/ai-anthropic + +## 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-smol/packages/ai/anthropic/codegen.yml b/.repos/effect-smol/packages/ai/anthropic/codegen.yml new file mode 100644 index 00000000000..a39a97db8af --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/codegen.yml @@ -0,0 +1,63 @@ +# 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: + # Remove string from Model schema - only keep literal model IDs + - '[{"op":"remove","path":"/components/schemas/Model/anyOf/0"}]' + # 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 }))," \ No newline at end of file diff --git a/.repos/effect-smol/packages/ai/anthropic/docgen.json b/.repos/effect-smol/packages/ai/anthropic/docgen.json new file mode 100644 index 00000000000..bc0548cd391 --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/docgen.json @@ -0,0 +1,26 @@ +{ + "$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"], + "tscExecutable": "tsgo", + "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-smol/packages/ai/anthropic/package.json b/.repos/effect-smol/packages/ai/anthropic/package.json new file mode 100644 index 00000000000..dffe1c21306 --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/package.json @@ -0,0 +1,67 @@ +{ + "name": "@effect/ai-anthropic", + "version": "4.0.0-beta.73", + "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-smol.git", + "directory": "packages/ai/anthropic" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect-smol/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", + "build:tsgo": "tsgo -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-smol/packages/ai/anthropic/src/AnthropicClient.ts b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicClient.ts new file mode 100644 index 00000000000..0c0a4d894de --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicClient.ts @@ -0,0 +1,471 @@ +/** + * The `AnthropicClient` module defines the low-level Effect service used to + * call Anthropic's API. It wraps the generated Anthropic HTTP client with + * Effect layers, request defaults, authentication headers, API versioning, + * response decoding, and error mapping for Messages API calls. + * + * **Mental model** + * + * `HttpClient.HttpClient` provides the transport. {@link make} turns explicit + * {@link Options} into an {@link AnthropicClient} service, while {@link layer} + * and {@link layerConfig} provide that service as a layer. The service exposes + * the generated client for direct endpoint access plus handwritten helpers for + * regular and streaming message creation. + * + * **Common tasks** + * + * - Provide an authenticated Anthropic client from an API key and optional base + * URL + * - Load client settings from Effect `Config` with {@link layerConfig} + * - Apply HTTP client transformations for proxying, retries, instrumentation, + * or tests + * - Decode Anthropic server-sent event streams into typed message events + * + * **Gotchas** + * + * - `apiKey` is optional so proxied and test clients can provide + * authentication elsewhere. + * - `createMessageStream` filters Anthropic ping events and terminates the + * stream when a `message_stop` event is received. + * - The message helpers map transport, schema, and provider failures to the + * unified Effect AI error type. + * + * @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: < + Type extends { + readonly id?: string | undefined + readonly event: string + readonly data: string + }, + DecodingServices + >( + schema: Schema.Decoder + ) => (request: HttpClientRequest.HttpClientRequest) => Stream.Stream< + Type, + HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry, + 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 models + * @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 = < + Type extends { + readonly id?: string | undefined + readonly event: string + readonly data: string + }, + DecodingServices + >(schema: Schema.Decoder) => + (request: HttpClientRequest.HttpClientRequest): Stream.Stream< + Type, + HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry, + 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-smol/packages/ai/anthropic/src/AnthropicConfig.ts b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicConfig.ts new file mode 100644 index 00000000000..101867b3dbf --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicConfig.ts @@ -0,0 +1,97 @@ +/** + * The `AnthropicConfig` module provides contextual configuration for the + * Anthropic AI provider integration. It is used to customize the generated + * Anthropic HTTP client without changing individual request code. + * + * **Common tasks** + * + * - Provide a shared `HttpClient` transformation for Anthropic requests + * - Add provider-specific concerns such as request instrumentation, proxying, + * retries, or header manipulation + * - Scope a client transformation to a single effect with {@link withClientTransform} + * + * **Gotchas** + * + * - Configuration is read from the Effect context, so overrides only apply to + * effects run inside the configured scope + * - `withClientTransform` replaces the current `transformClient` value while + * preserving any other Anthropic configuration fields + * + * @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 a layer or integration needs to provide or read Anthropic client + * configuration through Effect's context. + * + * @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-smol/packages/ai/anthropic/src/AnthropicError.ts b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicError.ts new file mode 100644 index 00000000000..d7a521907e9 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/anthropic/src/AnthropicLanguageModel.ts b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicLanguageModel.ts new file mode 100644 index 00000000000..752bff242c9 --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicLanguageModel.ts @@ -0,0 +1,3113 @@ +/** + * The `AnthropicLanguageModel` module provides the Anthropic implementation of + * Effect AI's `LanguageModel` service. It turns Effect AI prompts, tools, files, + * reasoning parts, and provider options into Anthropic Messages API requests, + * and converts Anthropic responses and streams back into Effect AI response + * parts with Anthropic-specific metadata. + * + * **When to use** + * + * Use when create an Anthropic-backed model with {@link model} + * - Build or provide a `LanguageModel.LanguageModel` layer with {@link layer} + * or {@link make} + * - Supply default request options through {@link Config} + * - Override configuration for a scoped operation with {@link withConfigOverride} + * - Attach Anthropic provider options for prompt caching, document citations, + * reasoning signatures, MCP metadata, and server-side tools + * + * **Gotchas** + * + * - Prompt files are translated to Anthropic image or document blocks; only the + * supported media types can be sent to the provider. + * - Structured output support depends on the selected Claude model, so this + * module may use Anthropic's native structured output or fall back to a JSON + * response tool. + * - Some features require Anthropic beta headers, which are added + * automatically from the selected tools, files, and model capabilities. + * + * @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. + * + * **Details** + * + * The Anthropic language model constructors accept `Model` values and custom + * string model ids, so this type is best used for autocomplete and type checking + * of known Claude ids. + * + * @category models + * @since 4.0.0 + */ +export type Model = typeof Generated.Model.Type + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * Context service for Anthropic language model configuration. + * + * **When to use** + * + * Use when you need to provide or override Anthropic model configuration on a + * per-request basis via `Context.Service`. + * + * **Details** + * + * This service can be used to provide default configuration values or to + * override configuration on a per-request basis. + * + * @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 an Effect needs to construct a `LanguageModel.Service` value backed + * by `AnthropicClient`. + * + * **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 : undefined + } + : undefined + + const params = yield* transformToolCallParams(options.tools, part.name, part.input) + + parts.push({ + type: "tool-call", + id: part.id, + name: part.name, + 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 + + parts.push({ + type: "tool-params-start", + id: part.id, + name: part.name + }) + + 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, part.name, part.input) + + parts.push({ + type: "tool-call", + id: part.id, + name: part.name, + 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) : "" + contentBlocks.set(event.index, { + type: "tool-call", + id: part.id, + name: part.name, + params: initialParams, + firstDelta: initialParams.length > 0, + ...(Predicate.isNotUndefined(caller) ? { caller } : undefined) + }) + + parts.push({ + type: "tool-params-start", + id: part.id, + name: part.name + }) + + 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") + ) { + 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-smol/packages/ai/anthropic/src/AnthropicTelemetry.ts b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicTelemetry.ts new file mode 100644 index 00000000000..aa24d292fc6 --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicTelemetry.ts @@ -0,0 +1,162 @@ +/** + * The `AnthropicTelemetry` module adds Anthropic-specific attributes to the + * provider-neutral GenAI telemetry model. It keeps the standard + * `Telemetry.addGenAIAnnotations` attributes and adds Anthropic request and + * response metadata under the `gen_ai.anthropic.*` OpenTelemetry namespaces. + * + * **Mental model** + * + * - Standard GenAI attributes come from `effect/unstable/ai/Telemetry` + * - Anthropic request attributes are written under + * `gen_ai.anthropic.request.*` + * - Anthropic response attributes are written under + * `gen_ai.anthropic.response.*` + * - Attribute option keys are written in camelCase and converted to + * OpenTelemetry snake_case attribute names + * - {@link addGenAIAnnotations} mutates the supplied span by adding any + * non-nullish attributes from the option object + * + * **Common tasks** + * + * - Use {@link AnthropicTelemetryAttributes} when typing the complete set of + * standard and Anthropic-specific span attributes + * - Pass `anthropic.request` data for options such as extended thinking and + * thinking budget tokens + * - Pass `anthropic.response` data for response details such as stop reason and + * cache token counts + * - Use {@link addGenAIAnnotations} from an Anthropic model span to keep + * standard GenAI and provider-specific annotations together + * + * **Gotchas** + * + * - This module only annotates spans; it does not start spans or export traces + * - Null and undefined attribute values are skipped instead of being written + * - The helper accepts both direct and data-last forms because it is built with + * `dual` + * + * @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 models + * @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 utils + * @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-smol/packages/ai/anthropic/src/AnthropicTool.ts b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicTool.ts new file mode 100644 index 00000000000..b168c167e5f --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/src/AnthropicTool.ts @@ -0,0 +1,2513 @@ +/** + * The `AnthropicTool` module defines Anthropic provider tools that can be + * attached to Anthropic-backed Effect AI language model requests. These are + * provider-defined tools: Anthropic owns the tool names, argument formats, + * beta headers, and in some cases the execution environment. + * + * **Mental model** + * + * - Exports such as {@link Bash_20250124}, {@link CodeExecution_20250825}, + * {@link ComputerUse_20250124}, and {@link WebSearch_20250305} create + * versioned provider-defined tool values understood by the Anthropic + * language model integration + * - Tool-specific `Schema` exports describe the arguments Claude may provide + * when invoking that provider tool + * - Some tools run on Anthropic infrastructure, such as + * {@link WebSearch_20250305}, {@link WebFetch_20250910}, and + * {@link CodeExecution_20250825}; handler-backed tools such as Bash, + * Computer Use, and Text Editor variants require application-side execution + * - Selecting a versioned tool lets the Anthropic model layer add the beta + * header required by that exact Anthropic API version + * + * **Common tasks** + * + * - Enable terminal-style actions with {@link Bash_20250124} + * - Enable sandboxed code execution with {@link CodeExecution_20250825} + * - Enable desktop automation payloads with {@link ComputerUse_20250124} + * - Enable persistent memory file operations with {@link Memory_20250818} + * - Enable text-editor commands with {@link TextEditor_20250728} + * - Enable hosted web capabilities with {@link WebSearch_20250305} or + * {@link WebFetch_20250910} + * - Restrict tool discovery with {@link ToolSearchRegex_20251119} or + * {@link ToolSearchBM25_20251119} + * + * **Quickstart** + * + * **Example** (Creating hosted Anthropic tools) + * + * ```ts + * import { AnthropicTool } from "@effect/ai-anthropic" + * + * const tools = [ + * AnthropicTool.WebSearch_20250305({ maxUses: 3 }), + * AnthropicTool.WebFetch_20250910({ + * citations: { enabled: true } + * }) + * ] + * ``` + * + * **Gotchas** + * + * - The suffix date is part of the Anthropic tool contract; choose the version + * that matches the model and beta behavior you intend to use + * - Handler-backed tools expose schemas for Claude's requested actions, but + * your runtime is responsible for performing those actions and returning + * results + * + * @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 need the model to execute bash commands and require the 2024-10-22 + * version of the 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.optional(Schema.Boolean) + }) +}) + +/** + * Defines the Anthropic Bash tool (2025-01-24 version). + * + * **When to use** + * + * Use when you need the model to execute bash commands and require the 2025-01-24 + * version of the 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.optional(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 to validate or construct the `view` command for Anthropic code execution + * text editor tool calls. + * + * **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 handling or validating the `view` command for Anthropic's text + * editor code execution tool. + * + * **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 validating or constructing the input payload for the 2025-08-25 + * Anthropic code execution 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 typing input passed to the 2025-08-25 Anthropic code execution tool. + * + * **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 need the model to execute code in a sandboxed environment and + * require the 2025-05-22 version of the 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 need the model to execute code in a sandboxed environment and + * require the 2025-08-25 version of the 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. + * + * **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. + * + * **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. + * + * **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.optional(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.optional(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.optional(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.optional(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.optional(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.optional(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.optional(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.optional(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. + * + * **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 configuring 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 configuring 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"` and a `path` string. + * + * @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 +}) +/** + * 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.optional(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", + 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.optional(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. + * + * **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. + * + * **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 configuring 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 configuring 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 configuring 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 providing location helps return more relevant 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 configuring `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 Claude should 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 configuring whether web fetch results should include citations. + * + * **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. + * + * **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 configuring `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. + * + * **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. + * + * **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 Claude should 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-smol/packages/ai/anthropic/src/Generated.ts b/.repos/effect-smol/packages/ai/anthropic/src/Generated.ts new file mode 100644 index 00000000000..1d2ebcb0e6a --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/src/Generated.ts @@ -0,0 +1,12600 @@ +/** + * @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 = + | "claude-opus-4-6" + | "claude-sonnet-4-6" + | "claude-opus-4-5-20251101" + | "claude-opus-4-5" + | "claude-3-7-sonnet-latest" + | "claude-3-7-sonnet-20250219" + | "claude-3-5-haiku-latest" + | "claude-3-5-haiku-20241022" + | "claude-haiku-4-5" + | "claude-haiku-4-5-20251001" + | "claude-sonnet-4-20250514" + | "claude-sonnet-4-0" + | "claude-4-sonnet-20250514" + | "claude-sonnet-4-5" + | "claude-sonnet-4-5-20250929" + | "claude-opus-4-0" + | "claude-opus-4-20250514" + | "claude-4-opus-20250514" + | "claude-opus-4-1-20250805" + | "claude-3-opus-latest" + | "claude-3-opus-20240229" + | "claude-3-haiku-20240307" +export const Model = Schema.Literals([ + "claude-opus-4-6", + "claude-sonnet-4-6", + "claude-opus-4-5-20251101", + "claude-opus-4-5", + "claude-3-7-sonnet-latest", + "claude-3-7-sonnet-20250219", + "claude-3-5-haiku-latest", + "claude-3-5-haiku-20241022", + "claude-haiku-4-5", + "claude-haiku-4-5-20251001", + "claude-sonnet-4-20250514", + "claude-sonnet-4-0", + "claude-4-sonnet-20250514", + "claude-sonnet-4-5", + "claude-sonnet-4-5-20250929", + "claude-opus-4-0", + "claude-opus-4-20250514", + "claude-4-opus-20250514", + "claude-opus-4-1-20250805", + "claude-3-opus-latest", + "claude-3-opus-20240229", + "claude-3-haiku-20240307" +]).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-smol/packages/ai/anthropic/src/index.ts b/.repos/effect-smol/packages/ai/anthropic/src/index.ts new file mode 100644 index 00000000000..48af730629d --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/anthropic/src/internal/errors.ts b/.repos/effect-smol/packages/ai/anthropic/src/internal/errors.ts new file mode 100644 index 00000000000..3745bccab4a --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/anthropic/src/internal/utilities.ts b/.repos/effect-smol/packages/ai/anthropic/src/internal/utilities.ts new file mode 100644 index 00000000000..320754c4dc0 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts b/.repos/effect-smol/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts new file mode 100644 index 00000000000..8255ec8e609 --- /dev/null +++ b/.repos/effect-smol/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts @@ -0,0 +1,241 @@ +import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Layer, Redacted, Schema, Stream } from "effect" +import { 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) + })) + }) + + 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) + })) + }) +}) + +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-smol/packages/ai/anthropic/tsconfig.json b/.repos/effect-smol/packages/ai/anthropic/tsconfig.json new file mode 100644 index 00000000000..19a2f5dbca4 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/anthropic/vitest.config.ts b/.repos/effect-smol/packages/ai/anthropic/vitest.config.ts new file mode 100644 index 00000000000..c8a52c1826e --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai-compat/CHANGELOG.md b/.repos/effect-smol/packages/ai/openai-compat/CHANGELOG.md new file mode 100644 index 00000000000..9af7ede7b26 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai-compat/CHANGELOG.md @@ -0,0 +1,550 @@ +# @effect/ai-openai-compat + +## 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-smol/packages/ai/openai-compat/docgen.json b/.repos/effect-smol/packages/ai/openai-compat/docgen.json new file mode 100644 index 00000000000..2734de35828 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai-compat/docgen.json @@ -0,0 +1,24 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/openai-compat/src/", + "exclude": ["src/internal/**"], + "tscExecutable": "tsgo", + "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-smol/packages/ai/openai-compat/package.json b/.repos/effect-smol/packages/ai/openai-compat/package.json new file mode 100644 index 00000000000..626985fb678 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai-compat/package.json @@ -0,0 +1,69 @@ +{ + "name": "@effect/ai-openai-compat", + "version": "4.0.0-beta.73", + "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-smol.git", + "directory": "packages/ai/openai-compat" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect-smol/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", + "build:tsgo": "tsgo -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-smol/packages/ai/openai-compat/src/OpenAiClient.ts b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiClient.ts new file mode 100644 index 00000000000..5ff316efaa2 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiClient.ts @@ -0,0 +1,1232 @@ +/** + * The `OpenAiClient` module provides an Effect service for calling + * OpenAI-compatible chat completions and embeddings APIs. It builds on the + * Effect HTTP client, adds authentication and OpenAI header handling, and + * exposes typed helpers for regular responses, server-sent event streaming, and + * embedding requests. + * + * **Common tasks** + * + * - Create a client service directly with {@link make} + * - Provide the service as a layer with {@link layer} or {@link layerConfig} + * - Send non-streaming chat completion requests with `createResponse` + * - Send streaming chat completion requests with `createResponseStream` + * - Generate embeddings with `createEmbedding` + * - Reuse the exported request and response types when integrating compatible providers + * + * **Gotchas** + * + * - The default base URL is `https://api.openai.com/v1`; set `apiUrl` for other + * OpenAI-compatible providers. + * - `createResponseStream` forces `stream: true` and requests usage events with + * `stream_options.include_usage`. + * - HTTP and schema decoding failures are mapped 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, 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 models + * @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 to construct the OpenAI-compatible client service inside an effect when + * you need the service value directly. + * + * **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 OpenAI-compatible client settings should 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({ + name: Schema.optionalKey(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)), + tool_calls: Schema.optionalKey(Schema.Array(ChatCompletionToolCall)) +}) + +const ChatCompletionDelta = Schema.Struct({ + role: Schema.optionalKey(Schema.String), + 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-smol/packages/ai/openai-compat/src/OpenAiConfig.ts b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiConfig.ts new file mode 100644 index 00000000000..6f7e3808bd3 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiConfig.ts @@ -0,0 +1,102 @@ +/** + * The `OpenAiConfig` module provides shared configuration for clients that + * talk to OpenAI-compatible APIs. It is used to customize the HTTP client + * wiring around a provider without changing the higher-level model, + * embeddings, or tool-calling APIs that consume the client. + * + * **Common tasks** + * + * - Install a client transform with {@link withClientTransform} + * - Add provider-specific HTTP behavior, such as headers, retries, proxies, or + * instrumentation + * - Read the active configuration from the Effect context when implementing + * OpenAI-compatible integrations + * + * **Gotchas** + * + * - The transform receives and returns an `HttpClient`, so it should preserve + * the existing client behavior unless it intentionally replaces it + * - Configuration is provided through Effect context and is scoped to the + * effect that receives the service + * + * @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 OpenAI-compatible client configuration when you + * need to provide or read scoped HTTP client transforms through Effect context. + * + * @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-smol/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts new file mode 100644 index 00000000000..7c831d2b9cf --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts @@ -0,0 +1,282 @@ +/** + * 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, and supports scoped request configuration overrides. + * + * **Mental model** + * + * - {@link make} builds the `EmbeddingModel` service when an + * {@link OpenAiClient} is already available. + * - {@link layer} provides that service as a `Layer`. + * - {@link model} creates an `AiModel` and also provides the configured vector + * dimensions service expected by embedding consumers. + * - {@link Config} and {@link withConfigOverride} merge default request options + * with scoped overrides for individual operations. + * + * **Common tasks** + * + * - Use {@link model} when application code wants a named `AiModel` with known + * dimensions. + * - Use {@link layer} when composing services around an existing + * {@link OpenAiClient} layer. + * - Use {@link withConfigOverride} to set per-operation options such as `user`, + * `dimensions`, or provider-specific request fields. + * + * **Gotchas** + * + * - {@link Model} is `string` because OpenAI-compatible providers use their + * own embedding model identifiers. + * - {@link model} requires explicit dimensions because compatible providers do + * not expose a shared way to infer vector width. + * - Provider responses must contain one numeric vector per input with unique, + * in-range indexes; base64 embeddings or malformed indexes fail with + * `InvalidOutputError`. + * + * @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 + +/** + * 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, + Simplify< + & Partial< + Omit< + CreateEmbeddingRequestJson, + "input" + > + > + & { + 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: { + 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-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?: 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 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?: Omit | 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-smol/packages/ai/openai-compat/src/OpenAiError.ts b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiError.ts new file mode 100644 index 00000000000..88ba2dd9f5a --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai-compat/src/OpenAiLanguageModel.ts b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiLanguageModel.ts new file mode 100644 index 00000000000..6d4790ea53f --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiLanguageModel.ts @@ -0,0 +1,1937 @@ +/** + * The `OpenAiLanguageModel` module adapts OpenAI-compatible chat-completions + * providers to the shared Effect AI `LanguageModel` interface. It translates + * provider-neutral prompts, tools, structured output schemas, and streaming + * responses into the request and response shapes used by `OpenAiClient`. + * + * Use this module when an application wants to talk to OpenAI-compatible + * endpoints through Effect AI abstractions rather than constructing provider + * payloads directly. The exported constructors build a language model service + * from a model id, while `Config` and {@link withConfigOverride} provide scoped + * defaults for request fields such as temperature, reasoning options, text + * format, and provider-specific file handling. + * + * **Common tasks** + * + * - Create a model descriptor with {@link model} + * - Build or provide the `LanguageModel` service with {@link make} or + * {@link layer} + * - Scope request defaults with {@link Config} and {@link withConfigOverride} + * - Send tool calls, structured output schemas, images, files, and reasoning + * metadata through the provider-neutral Effect AI prompt types + * + * **Gotchas** + * + * - The module requires an `OpenAiClient` service; configure authentication, + * base URL, and HTTP behavior through that client layer. + * - Compatibility depends on the provider supporting the OpenAI request fields + * being used. Optional capabilities such as strict JSON schemas, reasoning + * metadata, and tool status fields may vary across providers. + * - `fileIdPrefixes` tells the prompt conversion which file references are + * provider file IDs instead of base64 file contents. + * + * @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 +// ============================================================================= + +/** + * 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, + Simplify< + & Partial< + Omit< + CreateResponse, + "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 + 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?: 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, +// 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 when an Effect needs to construct a `LanguageModel.Service` value 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?: 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!) + 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?: Omit | 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, + // @effect-diagnostics-next-line preferSchemaOverJson:off + 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, + // @effect-diagnostics-next-line preferSchemaOverJson:off + 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) { + 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 hasToolCalls = false + const activeToolCalls: Record = {} + + return stream.pipe( + Stream.mapEffect(Effect.fnUntraced(function*(event) { + const parts: Array = [] + + if (event === "[DONE]") { + 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` + 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 + } + + if (choice.delta?.content !== undefined && Predicate.isNotNull(choice.delta.content)) { + 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 = providerToolName !== undefined + ? 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 | null => { + 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.length > 0 ? text : null +} + +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-smol/packages/ai/openai-compat/src/OpenAiTelemetry.ts b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiTelemetry.ts new file mode 100644 index 00000000000..85369f862f4 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai-compat/src/OpenAiTelemetry.ts @@ -0,0 +1,191 @@ +/** + * The `OpenAiTelemetry` module adds OpenAI-compatible provider attributes to + * the provider-neutral GenAI telemetry model. It keeps the standard + * `Telemetry.addGenAIAnnotations` attributes and adds OpenAI request and + * response metadata under the `gen_ai.openai.*` OpenTelemetry namespaces. + * + * **Mental model** + * + * - Standard GenAI attributes come from `effect/unstable/ai/Telemetry` + * - OpenAI request attributes are written under `gen_ai.openai.request.*` + * - OpenAI response attributes are written under `gen_ai.openai.response.*` + * - Attribute option keys are written in camelCase and converted to + * OpenTelemetry snake_case attribute names + * - {@link addGenAIAnnotations} mutates the supplied span by adding any + * non-nullish attributes from the option object + * + * **Common tasks** + * + * - Use {@link OpenAiTelemetryAttributes} when typing the complete set of + * standard and OpenAI-specific span attributes + * - Pass `openai.request` data for requested response format and service tier + * - Pass `openai.response` data for the service tier actually used and the + * system fingerprint returned by the provider + * - Use {@link addGenAIAnnotations} from an OpenAI-compatible model span to keep + * standard GenAI and provider-specific annotations together + * + * **Gotchas** + * + * - This module only annotates spans; it does not start spans or export traces + * - Null and undefined attribute values are skipped instead of being written + * - OpenAI-compatible providers may not return every OpenAI-specific response + * field, so only pass fields that are present on the provider response + * + * @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 models + * @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** + * + * This method will mutate the `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-smol/packages/ai/openai-compat/src/index.ts b/.repos/effect-smol/packages/ai/openai-compat/src/index.ts new file mode 100644 index 00000000000..7a5cd7f19e2 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai-compat/src/internal/errors.ts b/.repos/effect-smol/packages/ai/openai-compat/src/internal/errors.ts new file mode 100644 index 00000000000..2636d003a89 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai-compat/src/internal/utilities.ts b/.repos/effect-smol/packages/ai/openai-compat/src/internal/utilities.ts new file mode 100644 index 00000000000..da7855d88d4 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai-compat/src/internal/utilities.ts @@ -0,0 +1,33 @@ +import type * as Response from "effect/unstable/ai/Response" + +/** @internal */ +export const ProviderOptionsKey = "@effect/ai-openai-compat/OpenAiLanguageModel/ProviderOptions" + +/** @internal */ +export const ProviderMetadataKey = "@effect/ai-openai-compat/OpenAiLanguageModel/ProviderMetadata" + +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-smol/packages/ai/openai-compat/test/OpenAiClient.test.ts b/.repos/effect-smol/packages/ai/openai-compat/test/OpenAiClient.test.ts new file mode 100644 index 00000000000..2d89ffb26b7 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts b/.repos/effect-smol/packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts new file mode 100644 index 00000000000..b80e664f492 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts b/.repos/effect-smol/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts new file mode 100644 index 00000000000..f91a9005832 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts @@ -0,0 +1,1222 @@ +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("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 + }) + })) + }) + + 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.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")) + })) + }) + + 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-smol/packages/ai/openai-compat/tsconfig.json b/.repos/effect-smol/packages/ai/openai-compat/tsconfig.json new file mode 100644 index 00000000000..19a2f5dbca4 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai-compat/vitest.config.ts b/.repos/effect-smol/packages/ai/openai-compat/vitest.config.ts new file mode 100644 index 00000000000..c8a52c1826e --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai/CHANGELOG.md b/.repos/effect-smol/packages/ai/openai/CHANGELOG.md new file mode 100644 index 00000000000..b08f36d6f32 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/CHANGELOG.md @@ -0,0 +1,596 @@ +# @effect/ai-openai + +## 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-smol/packages/ai/openai/codegen.yaml b/.repos/effect-smol/packages/ai/openai/codegen.yaml new file mode 100644 index 00000000000..19d06da1667 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai/docgen.json b/.repos/effect-smol/packages/ai/openai/docgen.json new file mode 100644 index 00000000000..22eff2cf7c4 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/docgen.json @@ -0,0 +1,24 @@ +{ + "$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"], + "tscExecutable": "tsgo", + "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-smol/packages/ai/openai/package.json b/.repos/effect-smol/packages/ai/openai/package.json new file mode 100644 index 00000000000..49f96519c0e --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/package.json @@ -0,0 +1,67 @@ +{ + "name": "@effect/ai-openai", + "version": "4.0.0-beta.73", + "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-smol.git", + "directory": "packages/ai/openai" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect-smol/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", + "build:tsgo": "tsgo -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-smol/packages/ai/openai/src/Generated.ts b/.repos/effect-smol/packages/ai/openai/src/Generated.ts new file mode 100644 index 00000000000..c477b35abfe --- /dev/null +++ b/.repos/effect-smol/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.Json)] +) +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.Decoder + ) => + ( + 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-smol/packages/ai/openai/src/OpenAiClient.ts b/.repos/effect-smol/packages/ai/openai/src/OpenAiClient.ts new file mode 100644 index 00000000000..e2afa4a9002 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/src/OpenAiClient.ts @@ -0,0 +1,755 @@ +/** + * The `OpenAiClient` module provides the handwritten Effect service used by + * the OpenAI integration for Responses API and embedding requests. It builds on + * the Effect HTTP client, applies OpenAI authentication and organization or + * project headers, decodes the minimal schemas needed by higher-level modules, + * and maps transport or decoding failures into `AiError`. + * + * The service exposes a configured HTTP client plus helpers for non-streaming + * responses, server-sent event response streams, and embeddings. It also + * includes WebSocket mode for response streams when an application wants to use + * OpenAI's WebSocket transport instead of the default SSE path. + * + * **Common tasks** + * + * - Construct the service directly with {@link make} + * - Provide the service with {@link layer} or load settings from `Config` with + * {@link layerConfig} + * - Call `createResponse`, `createResponseStream`, or `createEmbedding` from + * code that depends on the `OpenAiClient` service + * - Enable WebSocket streaming around an effect with {@link withWebSocketMode} + * or through layers with {@link layerWebSocketMode} + * + * **Gotchas** + * + * - The default base URL is `https://api.openai.com/v1`; set `apiUrl` for + * proxies, local gateways, or compatible deployments. + * - A constructor `transformClient` is applied when the service is built, while + * scoped `OpenAiConfig` transforms are applied by request helpers when they + * run. + * - WebSocket mode requires a supported `Socket.WebSocketConstructor` layer and + * serializes response streams through the shared socket service. + * - This module is intentionally narrower than the generated OpenAI client; use + * `OpenAiClientGenerated` for direct access to generated endpoint helpers. + * + * @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 models + * @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 to construct the OpenAI client service inside an effect when you need the + * service value directly. + * + * **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 client settings should 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 code needs 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` + * + * This is because it needs to use non-standard options for setting 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` + * + * This is because it needs to use non-standard options for setting 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-smol/packages/ai/openai/src/OpenAiClientGenerated.ts b/.repos/effect-smol/packages/ai/openai/src/OpenAiClientGenerated.ts new file mode 100644 index 00000000000..53fbc3bdbe3 --- /dev/null +++ b/.repos/effect-smol/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 models + */ +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-smol/packages/ai/openai/src/OpenAiConfig.ts b/.repos/effect-smol/packages/ai/openai/src/OpenAiConfig.ts new file mode 100644 index 00000000000..02cc86ac14f --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/src/OpenAiConfig.ts @@ -0,0 +1,118 @@ +/** + * The `OpenAiConfig` module carries request-time configuration for the + * `@effect/ai-openai` package through Effect's context. It currently exposes a + * scoped HTTP client transform used by OpenAI request helpers when they execute + * provider calls. + * + * **Mental model** + * + * Client constructors set the baseline HTTP client for an OpenAI service. + * `OpenAiConfig` is for narrower dynamic scopes: wrap an effect with + * {@link withClientTransform} and OpenAI requests evaluated inside that effect + * see the transformed `HttpClient`. Leaving the scope restores the previous + * configuration. + * + * **Common tasks** + * + * - Add request middleware, tracing, retry policy, proxy routing, or test + * interception around a group of OpenAI calls. + * - Apply a temporary HTTP client transform without rebuilding the OpenAI + * service layer. + * - Share one transform across all OpenAI helpers executed inside the same + * Effect scope. + * + * **Gotchas** + * + * - Only one `transformClient` value is stored in the scoped config. Compose + * transforms into a single `HttpClient => HttpClient` function when multiple + * behaviors should apply. + * - The transform affects OpenAI requests that read this contextual config; it + * does not mutate an already constructed HTTP client globally. + * + * @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 a single effect or workflow needs temporary OpenAI HTTP client + * customization 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-smol/packages/ai/openai/src/OpenAiEmbeddingModel.ts b/.repos/effect-smol/packages/ai/openai/src/OpenAiEmbeddingModel.ts new file mode 100644 index 00000000000..a16addbed4a --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/src/OpenAiEmbeddingModel.ts @@ -0,0 +1,283 @@ +/** + * The `OpenAiEmbeddingModel` module provides the OpenAI implementation of + * Effect AI's `EmbeddingModel` service. It adapts the OpenAI embeddings + * endpoint into Effect AI's batch embedding interface, preserving input order + * and returning numeric vectors for each requested input. + * + * **Mental model** + * + * `OpenAiClient` owns transport, authentication, and provider request + * execution. This module owns embedding-specific configuration, response + * validation, and the `EmbeddingModel.EmbeddingModel` layer. Use {@link model} + * when you want an `AiModel` descriptor that also provides the configured + * embedding dimensions, or {@link layer} / {@link make} when the dimensions are + * managed separately. + * + * **Common tasks** + * + * - Provide an OpenAI-backed `EmbeddingModel.EmbeddingModel` from an existing + * `OpenAiClient` + * - Configure the OpenAI embedding model id, dimensions, encoding format, and + * other create-embedding request fields + * - Scope per-request defaults with {@link Config} and + * {@link withConfigOverride} + * + * **Gotchas** + * + * - The service expects OpenAI to return floating-point embedding arrays. + * Requesting base64 embeddings causes an `InvalidOutputError`. + * - Provider results are checked for missing, duplicate, or out-of-range + * indexes before they are exposed as Effect AI embedding results. + * + * @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 embedding requests need scoped OpenAI request defaults or overrides + * 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 or when the service + * value is needed directly. + * + * **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 a single effect or workflow needs scoped OpenAI embedding request + * defaults 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-smol/packages/ai/openai/src/OpenAiError.ts b/.repos/effect-smol/packages/ai/openai/src/OpenAiError.ts new file mode 100644 index 00000000000..cb0ccc2dd36 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai/src/OpenAiLanguageModel.ts b/.repos/effect-smol/packages/ai/openai/src/OpenAiLanguageModel.ts new file mode 100644 index 00000000000..ae460d5afb4 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/src/OpenAiLanguageModel.ts @@ -0,0 +1,3161 @@ +/** + * 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 + * responses and streams back into Effect AI response parts. + * + * **Mental model** + * + * `OpenAiClient` owns HTTP transport and provider calls. This module owns + * protocol translation: request assembly, tool choice conversion, structured + * output codecs, streaming event handling, response metadata, and GenAI + * telemetry annotations. {@link model}, {@link layer}, and {@link make} all + * build the same OpenAI-backed `LanguageModel.LanguageModel` service from a + * model id and optional request defaults. + * + * **Common tasks** + * + * - Provide an OpenAI-backed `LanguageModel.LanguageModel` from an existing + * `OpenAiClient` + * - Generate text or stream text through Effect AI's provider-neutral language + * model API + * - Use OpenAI provider metadata for files, reasoning items, tool calls, and + * response parts + * - Scope Responses API defaults with {@link Config} and + * {@link withConfigOverride} + * + * **Gotchas** + * + * - Some OpenAI model families receive system instructions as developer + * messages because the Responses API treats them differently. + * - File prompt parts are sent either as provider file ids or as base64 + * content, depending on `fileIdPrefixes`. + * - Structured output and tool-call behavior depends on both Effect AI tool + * definitions and OpenAI model capabilities. + * + * @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 when an Effect needs to construct a `LanguageModel.Service` value 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-smol/packages/ai/openai/src/OpenAiSchema.ts b/.repos/effect-smol/packages/ai/openai/src/OpenAiSchema.ts new file mode 100644 index 00000000000..9c4268e5680 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/src/OpenAiSchema.ts @@ -0,0 +1,1289 @@ +/** + * 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. + * + * **Mental model** + * + * - Request schemas such as {@link CreateResponse} and + * {@link CreateEmbeddingRequest} describe the encoded payloads sent to OpenAI. + * - Response schemas such as {@link Response}, {@link ResponseStreamEvent}, and + * {@link CreateEmbeddingResponse} describe provider data after schema + * decoding. + * - Shared schemas such as {@link InputItem}, {@link Tool}, and + * {@link TextResponseFormatConfiguration} cover the message, tool, and output + * format fragments reused across request and response shapes. + * + * **Common tasks** + * + * - Use {@link CreateResponse} with {@link Response} for non-streaming + * Responses API calls. + * - Use {@link ResponseStreamEvent} for server-sent events emitted by streaming + * Responses API calls. + * - Use {@link CreateEmbeddingRequest} and {@link CreateEmbeddingResponse} for + * embeddings endpoint payloads. + * - Use smaller schemas like {@link IncludeEnum}, {@link InputContent}, and + * {@link ToolChoice} when validating request fragments. + * + * **Gotchas** + * + * - The module models the subset of OpenAI shapes supported by this client + * path; it is not a complete mirror of every OpenAI REST API field. + * - Unknown future stream event types decode through + * {@link UnknownResponseStreamEvent}, while malformed known event types still + * fail schema decoding. + * + * @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. + * + * **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. + * + * **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. + * + * **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 models + * @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. + * + * **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. + * + * **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 to decode successful OpenAI embeddings responses. + * + * **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-smol/packages/ai/openai/src/OpenAiTelemetry.ts b/.repos/effect-smol/packages/ai/openai/src/OpenAiTelemetry.ts new file mode 100644 index 00000000000..c54cc88491a --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/src/OpenAiTelemetry.ts @@ -0,0 +1,179 @@ +/** + * OpenAI-specific telemetry annotations for GenAI spans. + * + * This module extends the provider-neutral GenAI telemetry model with + * attributes that only exist on OpenAI requests and responses. Use + * {@link addGenAIAnnotations} when an OpenAI operation already has an + * OpenTelemetry span and you want to record both standard GenAI metadata and + * OpenAI details such as response format, requested service tier, actual + * service tier, or system fingerprint. + * + * **Mental model** + * + * The public option shape stays idiomatic TypeScript (`responseFormat`, + * `serviceTier`, `systemFingerprint`). When annotations are applied, the + * attributes are written to the span under OpenTelemetry semantic-convention + * keys such as `gen_ai.openai.request.response_format` and + * `gen_ai.openai.response.system_fingerprint`. + * + * **Gotchas** + * + * Annotation mutates the provided span in place. It does not create, end, or + * export spans, and OpenAI-specific attributes are only added when the + * `openai.request` or `openai.response` option objects are present. + * + * @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 models + * @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** + * + * This method will mutate the `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-smol/packages/ai/openai/src/OpenAiTool.ts b/.repos/effect-smol/packages/ai/openai/src/OpenAiTool.ts new file mode 100644 index 00000000000..50e4716f7cf --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/src/OpenAiTool.ts @@ -0,0 +1,360 @@ +/** + * OpenAI provider-defined tools for Effect AI language model requests. + * + * This module exposes typed descriptors for OpenAI tools such as code + * interpreter, file search, image generation, MCP, web search, and shell-like + * local tools. Each descriptor captures the OpenAI provider name, user-facing + * configuration arguments, provider call parameters, success output schema, and + * whether the tool call needs an application-provided handler. + * + * **Mental model** + * + * Provider-defined tools are not implementations of the capability themselves. + * They are schemas and metadata that tell the language model provider which + * tool to make available and tell Effect AI how to decode any tool calls or + * tool results that come back. + * + * **Common tasks** + * + * Use hosted or provider-routed tools such as {@link CodeInterpreter}, + * {@link FileSearch}, {@link ImageGeneration}, {@link Mcp}, {@link WebSearch}, + * and {@link WebSearchPreview} to opt into OpenAI-managed capabilities. Use + * handler-required tools such as {@link ApplyPatch}, {@link Shell}, and + * {@link LocalShell} only when your application is prepared to execute the + * requested local operation and enforce its own safety policy. + * + * @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-smol/packages/ai/openai/src/index.ts b/.repos/effect-smol/packages/ai/openai/src/index.ts new file mode 100644 index 00000000000..b1758b61716 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai/src/internal/errors.ts b/.repos/effect-smol/packages/ai/openai/src/internal/errors.ts new file mode 100644 index 00000000000..a1be0c9633c --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai/src/internal/utilities.ts b/.repos/effect-smol/packages/ai/openai/src/internal/utilities.ts new file mode 100644 index 00000000000..fbb32dd084b --- /dev/null +++ b/.repos/effect-smol/packages/ai/openai/src/internal/utilities.ts @@ -0,0 +1,33 @@ +import type * as Response from "effect/unstable/ai/Response" + +/** @internal */ +export const ProviderOptionsKey = "@effect/ai-openai/OpenAiLanguageModel/ProviderOptions" + +/** @internal */ +export const ProviderMetadataKey = "@effect/ai-openai/OpenAiLanguageModel/ProviderMetadata" + +const finishReasonMap: Record = { + content_filter: "content-filter", + function_call: "tool-calls", + length: "length", + stop: "stop", + tool_calls: "tool-calls" +} + +/** @internal */ +export const 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-smol/packages/ai/openai/test/OpenAiClient.test.ts b/.repos/effect-smol/packages/ai/openai/test/OpenAiClient.test.ts new file mode 100644 index 00000000000..d9069fd5af7 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai/test/OpenAiEmbeddingModel.test.ts b/.repos/effect-smol/packages/ai/openai/test/OpenAiEmbeddingModel.test.ts new file mode 100644 index 00000000000..5bf08890ff5 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai/test/OpenAiLanguageModel.test.ts b/.repos/effect-smol/packages/ai/openai/test/OpenAiLanguageModel.test.ts new file mode 100644 index 00000000000..d85cb8644e0 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai/test/OpenAiSchema.test.ts b/.repos/effect-smol/packages/ai/openai/test/OpenAiSchema.test.ts new file mode 100644 index 00000000000..70a30f22c6d --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai/tsconfig.json b/.repos/effect-smol/packages/ai/openai/tsconfig.json new file mode 100644 index 00000000000..19a2f5dbca4 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openai/vitest.config.ts b/.repos/effect-smol/packages/ai/openai/vitest.config.ts new file mode 100644 index 00000000000..c8a52c1826e --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openrouter/CHANGELOG.md b/.repos/effect-smol/packages/ai/openrouter/CHANGELOG.md new file mode 100644 index 00000000000..7c8a0cbd0b7 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openrouter/CHANGELOG.md @@ -0,0 +1,537 @@ +# @effect/ai-openrouter + +## 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-smol/packages/ai/openrouter/codegen.yml b/.repos/effect-smol/packages/ai/openrouter/codegen.yml new file mode 100644 index 00000000000..b556693a366 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openrouter/docgen.json b/.repos/effect-smol/packages/ai/openrouter/docgen.json new file mode 100644 index 00000000000..00f806cd63f --- /dev/null +++ b/.repos/effect-smol/packages/ai/openrouter/docgen.json @@ -0,0 +1,24 @@ +{ + "$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"], + "tscExecutable": "tsgo", + "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-smol/packages/ai/openrouter/package.json b/.repos/effect-smol/packages/ai/openrouter/package.json new file mode 100644 index 00000000000..3d09d41cc0a --- /dev/null +++ b/.repos/effect-smol/packages/ai/openrouter/package.json @@ -0,0 +1,67 @@ +{ + "name": "@effect/ai-openrouter", + "version": "4.0.0-beta.73", + "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-smol.git", + "directory": "packages/ai/openrouter" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect-smol/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", + "build:tsgo": "tsgo -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-smol/packages/ai/openrouter/src/Generated.ts b/.repos/effect-smol/packages/ai/openrouter/src/Generated.ts new file mode 100644 index 00000000000..8cdb32d42d0 --- /dev/null +++ b/.repos/effect-smol/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.Decoder + ) => + ( + 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-smol/packages/ai/openrouter/src/OpenRouterClient.ts b/.repos/effect-smol/packages/ai/openrouter/src/OpenRouterClient.ts new file mode 100644 index 00000000000..621b64bebce --- /dev/null +++ b/.repos/effect-smol/packages/ai/openrouter/src/OpenRouterClient.ts @@ -0,0 +1,375 @@ +/** + * 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, typed errors, and streaming + * support. + * + * **Common tasks** + * + * - Build a client from explicit options with {@link make} + * - Provide the client to an application with {@link layer} or {@link layerConfig} + * - Create non-streaming chat completions with {@link Service.createChatCompletion} + * - Create server-sent event chat completion streams with + * {@link Service.createChatCompletionStream} + * - Customize authentication, base URL, OpenRouter ranking headers, or the + * underlying HTTP client through {@link Options} + * + * **Gotchas** + * + * - Streaming requests are sent directly to `/chat/completions` with `stream` + * and `stream_options.include_usage` enabled by this module. + * - OpenRouter API failures, HTTP client failures, and schema decoding failures + * are mapped into `AiError` values for the exported service methods. + * + * @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 models + * @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 to construct the OpenRouter client service inside an effect when you need + * the service value directly. + * + * **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 OpenRouter client settings should 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-smol/packages/ai/openrouter/src/OpenRouterConfig.ts b/.repos/effect-smol/packages/ai/openrouter/src/OpenRouterConfig.ts new file mode 100644 index 00000000000..7e6774e429e --- /dev/null +++ b/.repos/effect-smol/packages/ai/openrouter/src/OpenRouterConfig.ts @@ -0,0 +1,125 @@ +/** + * The `OpenRouterConfig` module provides scoped contextual configuration for + * OpenRouter request execution. It lets a workflow customize the HTTP client + * used by generated OpenRouter request methods without rebuilding the + * `OpenRouterClient` layer. + * + * **Mental model** + * + * - {@link OpenRouterConfig} is a context service carrying optional + * OpenRouter-specific request configuration + * - {@link withClientTransform} provides that service around one effect + * - Generated request methods read the current context and apply the transform + * to their `HttpClient` + * - The scoped configuration only applies while the returned effect is run + * + * **Common tasks** + * + * - Add request logging, retries, proxy routing, headers, or test doubles with + * {@link withClientTransform} + * - Scope OpenRouter client customization to one workflow without changing the + * shared client layer + * + * **Gotchas** + * + * - Each {@link withClientTransform} call replaces the current scoped + * transform for the supplied effect; compose transforms manually when both + * behaviors should apply + * - The transform receives and returns an `HttpClient`, so it should preserve + * the OpenRouter request contract while adding behavior around it + * - Streaming chat completion requests are sent directly by `OpenRouterClient` + * and do not read this scoped transform + * + * @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 a single effect or workflow needs temporary OpenRouter HTTP client + * customization 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-smol/packages/ai/openrouter/src/OpenRouterError.ts b/.repos/effect-smol/packages/ai/openrouter/src/OpenRouterError.ts new file mode 100644 index 00000000000..00db3860d3e --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openrouter/src/OpenRouterLanguageModel.ts b/.repos/effect-smol/packages/ai/openrouter/src/OpenRouterLanguageModel.ts new file mode 100644 index 00000000000..f2ef7d9f323 --- /dev/null +++ b/.repos/effect-smol/packages/ai/openrouter/src/OpenRouterLanguageModel.ts @@ -0,0 +1,1828 @@ +/** + * 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 telemetry annotations into OpenRouter chat + * completion requests, then converts responses and streams back into Effect AI + * response parts. + * + * **Mental model** + * + * `OpenRouterClient` owns HTTP transport and authentication. This module owns + * protocol translation: message assembly, tool conversion, structured output + * codec selection, streaming chunk handling, OpenRouter metadata round-trips, + * and GenAI telemetry annotations. {@link model}, {@link layer}, and + * {@link make} all build the same OpenRouter-backed + * `LanguageModel.LanguageModel` service from a model id and optional request + * defaults. + * + * **Common tasks** + * + * - Create an OpenRouter model descriptor for `Effect.provide`: {@link model} + * - Provide `LanguageModel.LanguageModel` as a `Layer`: {@link layer} + * - Construct the service effectfully from an existing `OpenRouterClient`: + * {@link make} + * - Supply or scope OpenRouter request defaults: {@link Config}, + * {@link withConfigOverride} + * - Preserve OpenRouter reasoning and file metadata across turns: + * {@link ReasoningDetails}, {@link FileAnnotation} + * + * **Gotchas** + * + * - OpenRouter routes to many underlying providers, so support for images, + * files, tools, structured outputs, caching, and reasoning metadata depends + * on the selected model and route. + * - Provider-specific prompt and response metadata lives under the `openrouter` + * option namespace so later requests can replay reasoning details and file + * annotations when the model supports them. + * - Provider-defined tools are not supported by this integration; requests that + * include them fail before reaching OpenRouter. + * + * @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 an Effect needs to construct a `LanguageModel.Service` value backed + * by `OpenRouterClient`. + * + * **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 + } + + 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 +}) + +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-smol/packages/ai/openrouter/src/index.ts b/.repos/effect-smol/packages/ai/openrouter/src/index.ts new file mode 100644 index 00000000000..2ea005eb195 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openrouter/src/internal/errors.ts b/.repos/effect-smol/packages/ai/openrouter/src/internal/errors.ts new file mode 100644 index 00000000000..d01128e128e --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openrouter/src/internal/utilities.ts b/.repos/effect-smol/packages/ai/openrouter/src/internal/utilities.ts new file mode 100644 index 00000000000..5df64c53ba8 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openrouter/tsconfig.json b/.repos/effect-smol/packages/ai/openrouter/tsconfig.json new file mode 100644 index 00000000000..a9b59a31836 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/ai/openrouter/vitest.config.ts b/.repos/effect-smol/packages/ai/openrouter/vitest.config.ts new file mode 100644 index 00000000000..c8a52c1826e --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/react/CHANGELOG.md b/.repos/effect-smol/packages/atom/react/CHANGELOG.md new file mode 100644 index 00000000000..e36f142acee --- /dev/null +++ b/.repos/effect-smol/packages/atom/react/CHANGELOG.md @@ -0,0 +1,529 @@ +# @effect/atom-react + +## 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-smol/packages/atom/react/LICENSE b/.repos/effect-smol/packages/atom/react/LICENSE new file mode 100644 index 00000000000..7f6fe480f77 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/react/README.md b/.repos/effect-smol/packages/atom/react/README.md new file mode 100644 index 00000000000..1cd2f2005db --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/react/docgen.json b/.repos/effect-smol/packages/atom/react/docgen.json new file mode 100644 index 00000000000..d4143687693 --- /dev/null +++ b/.repos/effect-smol/packages/atom/react/docgen.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/atom/react/src/", + "exclude": ["src/internal/**/*.ts"], + "tscExecutable": "tsgo", + "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-smol/packages/atom/react/package.json b/.repos/effect-smol/packages/atom/react/package.json new file mode 100644 index 00000000000..56ae2edbe45 --- /dev/null +++ b/.repos/effect-smol/packages/atom/react/package.json @@ -0,0 +1,79 @@ +{ + "name": "@effect/atom-react", + "version": "4.0.0-beta.73", + "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-smol.git", + "directory": "packages/atom/react" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect-smol/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", + "build:tsgo": "tsgo -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.14", + "@types/react-dom": "^19.2.2", + "@types/scheduler": "^0.26.0", + "effect": "workspace:^", + "jsdom": "^29.1.1", + "react": "19.2.6", + "react-dom": "19.2.6", + "react-error-boundary": "^6.1.1", + "scheduler": "^0.27.0" + } +} diff --git a/.repos/effect-smol/packages/atom/react/src/Hooks.ts b/.repos/effect-smol/packages/atom/react/src/Hooks.ts new file mode 100644 index 00000000000..1345501f7ca --- /dev/null +++ b/.repos/effect-smol/packages/atom/react/src/Hooks.ts @@ -0,0 +1,489 @@ +/** + * React hooks for reading, writing, mounting, refreshing, and subscribing to + * Effect atoms from the registry provided by `RegistryContext`. + * + * **Common tasks** + * + * - Read atom values in React components with {@link useAtomValue} + * - Read and write writable atoms with {@link useAtom} + * - Write without subscribing to the value with {@link useAtomSet} + * - Seed registry-local initial values with {@link useAtomInitialValues} + * - Integrate `AsyncResult` atoms with React Suspense through {@link useAtomSuspense} + * - Subscribe to atom changes or derive stable `AtomRef` properties + * + * **Gotchas** + * + * - Hooks use the current `RegistryContext`, so each provider has an independent atom registry + * - Writable atoms are mounted by the write-oriented hooks before updates are sent + * - Suspense support throws promises for initial or waiting `AsyncResult` values and defects for failures unless `includeFailure` is enabled + * + * @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-smol/packages/atom/react/src/ReactHydration.ts b/.repos/effect-smol/packages/atom/react/src/ReactHydration.ts new file mode 100644 index 00000000000..f1adc2eddf7 --- /dev/null +++ b/.repos/effect-smol/packages/atom/react/src/ReactHydration.ts @@ -0,0 +1,124 @@ +/** + * React helpers for hydrating Atom registry state that was serialized on the + * server or produced by a previous render. This module exposes + * {@link HydrationBoundary}, a client component that receives dehydrated Atom + * values and applies them to the nearest {@link RegistryContext} before + * rendering children when it is safe to do so. + * + * **Common use cases** + * + * - Reusing Atom values that were collected during server rendering + * - Restoring client-side Atom state around a routed subtree + * - Keeping Atom-backed React trees consistent during hydration and transitions + * + * **React gotchas** + * + * - New Atom values can be hydrated during render so children see them + * immediately. + * - Existing Atom values are queued until after commit to avoid updating the + * current UI with transition data that might later be discarded. + * - Hydration is idempotent, so repeated or older dehydrated values are safe to + * pass through the boundary. + * + * @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-smol/packages/atom/react/src/RegistryContext.ts b/.repos/effect-smol/packages/atom/react/src/RegistryContext.ts new file mode 100644 index 00000000000..908b07626f0 --- /dev/null +++ b/.repos/effect-smol/packages/atom/react/src/RegistryContext.ts @@ -0,0 +1,125 @@ +/** + * The `RegistryContext` module provides the React context used by Effect Atom + * hooks to share an `AtomRegistry` across a component tree. The registry owns + * atom state, scheduling, and idle cleanup, so components that read or write + * atoms can coordinate through the same runtime instead of each creating an + * isolated registry. + * + * **Common tasks** + * + * - Use {@link RegistryProvider} to scope atom state to a React subtree + * - Seed atoms for tests, stories, or server-provided data with `initialValues` + * - Override scheduling or idle timing for custom rendering environments + * - Read {@link RegistryContext} when integrating lower-level atom APIs + * + * **Gotchas** + * + * - This is a client module because it depends on React runtime hooks and the + * scheduler package + * - A provider keeps the registry stable across renders and disposes it shortly + * after unmount, allowing React remounts to reuse the same registry + * - Overriding `scheduleTask` changes when atom work is flushed, so it should + * return a cancellation function compatible with React unmounts + * + * @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-smol/packages/atom/react/src/ScopedAtom.ts b/.repos/effect-smol/packages/atom/react/src/ScopedAtom.ts new file mode 100644 index 00000000000..82db157f306 --- /dev/null +++ b/.repos/effect-smol/packages/atom/react/src/ScopedAtom.ts @@ -0,0 +1,163 @@ +/** + * The `ScopedAtom` module provides a small React integration for creating atom + * instances that are scoped to a component subtree. A scoped atom bundles a + * React provider, context, and `use` accessor so each mounted provider owns its + * own atom instance instead of sharing a single module-level atom. + * + * Use `ScopedAtom` when an atom needs to be isolated per feature, route, + * component instance, test harness, or provider input. The provider may receive + * an initial `value` that is passed to the atom factory, making it useful for + * state that should be seeded from React props while still being consumed by the + * atom hooks in descendants. + * + * **Gotchas** + * + * - `use` must be called under the matching provider or it throws. + * - The provider creates the atom once for its lifetime; changing the provider + * `value` prop after mount does not recreate the atom. + * + * @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-smol/packages/atom/react/src/index.ts b/.repos/effect-smol/packages/atom/react/src/index.ts new file mode 100644 index 00000000000..554a0df3097 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/react/test/index.test.tsx b/.repos/effect-smol/packages/atom/react/test/index.test.tsx new file mode 100644 index 00000000000..df4d8ca9dbf --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/react/tsconfig.json b/.repos/effect-smol/packages/atom/react/tsconfig.json new file mode 100644 index 00000000000..19a2f5dbca4 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/react/vitest.config.ts b/.repos/effect-smol/packages/atom/react/vitest.config.ts new file mode 100644 index 00000000000..d9ba9429e67 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/react/vitest.setup.ts b/.repos/effect-smol/packages/atom/react/vitest.setup.ts new file mode 100644 index 00000000000..5f520a53662 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/solid/CHANGELOG.md b/.repos/effect-smol/packages/atom/solid/CHANGELOG.md new file mode 100644 index 00000000000..18a845d3c6f --- /dev/null +++ b/.repos/effect-smol/packages/atom/solid/CHANGELOG.md @@ -0,0 +1,525 @@ +# @effect/atom-solid + +## 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-smol/packages/atom/solid/LICENSE b/.repos/effect-smol/packages/atom/solid/LICENSE new file mode 100644 index 00000000000..7f6fe480f77 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/solid/README.md b/.repos/effect-smol/packages/atom/solid/README.md new file mode 100644 index 00000000000..a4239bf9c12 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/solid/docgen.json b/.repos/effect-smol/packages/atom/solid/docgen.json new file mode 100644 index 00000000000..ba9091a04b1 --- /dev/null +++ b/.repos/effect-smol/packages/atom/solid/docgen.json @@ -0,0 +1,26 @@ +{ + "$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"], + "tscExecutable": "tsgo", + "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-smol/packages/atom/solid/package.json b/.repos/effect-smol/packages/atom/solid/package.json new file mode 100644 index 00000000000..85088af5fbc --- /dev/null +++ b/.repos/effect-smol/packages/atom/solid/package.json @@ -0,0 +1,72 @@ +{ + "name": "@effect/atom-solid", + "version": "4.0.0-beta.73", + "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-smol.git", + "directory": "packages/atom/solid" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect-smol/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", + "build:tsgo": "tsgo -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.12" + } +} diff --git a/.repos/effect-smol/packages/atom/solid/src/Hooks.ts b/.repos/effect-smol/packages/atom/solid/src/Hooks.ts new file mode 100644 index 00000000000..4f4e33083fb --- /dev/null +++ b/.repos/effect-smol/packages/atom/solid/src/Hooks.ts @@ -0,0 +1,375 @@ +/** + * The `Hooks` module provides SolidJS hooks for reading, writing, mounting, and + * subscribing to Effect atoms through the current Solid atom registry. + * + * **Common tasks** + * + * - Read an atom as a Solid accessor with {@link useAtomValue} + * - Read and write a writable atom with {@link useAtom} + * - Write without subscribing to the value with {@link useAtomSet} + * - Refresh or mount atoms from components with {@link useAtomRefresh} and {@link useAtomMount} + * - Convert `AsyncResult` atoms into Solid resources with {@link useAtomResource} + * - Work with atom refs and nested ref properties with {@link useAtomRef}, {@link useAtomRefProp}, + * and {@link useAtomRefPropValue} + * + * **Solid integration notes** + * + * Hooks in this module read the registry from {@link RegistryContext}, so they + * should be used under the matching provider for the atom graph you want to + * observe. Atom arguments are thunks so Solid can track dynamic atom selection; + * subscriptions are registered in Solid computations and disposed with + * `onCleanup` when the computation changes or the component unmounts. + * + * @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-smol/packages/atom/solid/src/RegistryContext.ts b/.repos/effect-smol/packages/atom/solid/src/RegistryContext.ts new file mode 100644 index 00000000000..b69f3160bbd --- /dev/null +++ b/.repos/effect-smol/packages/atom/solid/src/RegistryContext.ts @@ -0,0 +1,97 @@ +/** + * The `RegistryContext` module provides the Solid context used by Effect Atom + * hooks to share an `AtomRegistry` across an owner tree. The registry owns atom + * state, scheduling, and idle cleanup, so Solid components that read or write + * atoms coordinate through the same runtime instead of creating isolated + * registries. + * + * **Common tasks** + * + * - Use {@link RegistryProvider} to scope atom state to a Solid subtree + * - Seed atoms for tests, examples, or server-provided data with `initialValues` + * - Override scheduling or idle timing for custom rendering environments + * - Read {@link RegistryContext} when integrating lower-level atom APIs + * + * **Gotchas** + * + * - The provider creates a registry for the current Solid owner and disposes it + * with `onCleanup` + * - `initialValues` are applied when the provider creates the registry, not as + * reactive updates after creation + * - Overriding `scheduleTask` changes when atom work is flushed, so it should + * return a cancellation function that is safe to call during Solid cleanup + * + * @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 integrating lower-level Solid atom APIs that need direct access to, + * or direct provisioning of, the `AtomRegistry` for the current owner tree. + * + * **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-smol/packages/atom/solid/src/index.ts b/.repos/effect-smol/packages/atom/solid/src/index.ts new file mode 100644 index 00000000000..e8c448255ec --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/solid/test/index.test.tsx b/.repos/effect-smol/packages/atom/solid/test/index.test.tsx new file mode 100644 index 00000000000..cd90407be8d --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/solid/tsconfig.json b/.repos/effect-smol/packages/atom/solid/tsconfig.json new file mode 100644 index 00000000000..19a2f5dbca4 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/solid/vitest.config.ts b/.repos/effect-smol/packages/atom/solid/vitest.config.ts new file mode 100644 index 00000000000..48bb912d71e --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/solid/vitest.setup.ts b/.repos/effect-smol/packages/atom/solid/vitest.setup.ts new file mode 100644 index 00000000000..ce48643f799 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/vue/CHANGELOG.md b/.repos/effect-smol/packages/atom/vue/CHANGELOG.md new file mode 100644 index 00000000000..14aa994e9b0 --- /dev/null +++ b/.repos/effect-smol/packages/atom/vue/CHANGELOG.md @@ -0,0 +1,523 @@ +# @effect/atom-vue + +## 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-smol/packages/atom/vue/LICENSE b/.repos/effect-smol/packages/atom/vue/LICENSE new file mode 100644 index 00000000000..7f6fe480f77 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/vue/README.md b/.repos/effect-smol/packages/atom/vue/README.md new file mode 100644 index 00000000000..5ef604acc25 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/vue/docgen.json b/.repos/effect-smol/packages/atom/vue/docgen.json new file mode 100644 index 00000000000..a02edec8283 --- /dev/null +++ b/.repos/effect-smol/packages/atom/vue/docgen.json @@ -0,0 +1,24 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/atom/vue/src/", + "exclude": ["src/internal/**/*.ts"], + "tscExecutable": "tsgo", + "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-smol/packages/atom/vue/package.json b/.repos/effect-smol/packages/atom/vue/package.json new file mode 100644 index 00000000000..ee0b877ff46 --- /dev/null +++ b/.repos/effect-smol/packages/atom/vue/package.json @@ -0,0 +1,68 @@ +{ + "name": "@effect/atom-vue", + "version": "4.0.0-beta.73", + "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-smol.git", + "directory": "packages/atom/vue" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect-smol/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", + "build:tsgo": "tsgo -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.34" + }, + "peerDependencies": { + "effect": "workspace:^", + "vue": "^3.5.0" + } +} diff --git a/.repos/effect-smol/packages/atom/vue/src/index.ts b/.repos/effect-smol/packages/atom/vue/src/index.ts new file mode 100644 index 00000000000..799c9ebd46c --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/vue/test/index.test.ts b/.repos/effect-smol/packages/atom/vue/test/index.test.ts new file mode 100644 index 00000000000..79cf1df55c5 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/vue/tsconfig.json b/.repos/effect-smol/packages/atom/vue/tsconfig.json new file mode 100644 index 00000000000..d3687aaaaed --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/atom/vue/vitest.config.ts b/.repos/effect-smol/packages/atom/vue/vitest.config.ts new file mode 100644 index 00000000000..d5870de3d94 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/CHANGELOG.md b/.repos/effect-smol/packages/effect/CHANGELOG.md new file mode 100644 index 00000000000..565489dad69 --- /dev/null +++ b/.repos/effect-smol/packages/effect/CHANGELOG.md @@ -0,0 +1,1391 @@ +# effect + +## 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-smol/packages/effect/CONFIG.md b/.repos/effect-smol/packages/effect/CONFIG.md new file mode 100644 index 00000000000..2802070db08 --- /dev/null +++ b/.repos/effect-smol/packages/effect/CONFIG.md @@ -0,0 +1,583 @@ +# 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 root path segment for lookup. Omit it when the config is part of a larger `Config.schema`. + +## 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 Under a Prefix + +Prepends a path segment to every key the inner config reads: + +```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" +``` + +### `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")]) +``` + +## 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 + +### `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. + +### `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) +``` + +### `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. + +### `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"] +``` + +### `ConfigProvider.mapInput` — Arbitrary Path Transforms + +Transform path segments 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) +) +``` + +## 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-smol/packages/effect/HTTPAPI.md b/.repos/effect-smol/packages/effect/HTTPAPI.md new file mode 100644 index 00000000000..a693101dfa9 --- /dev/null +++ b/.repos/effect-smol/packages/effect/HTTPAPI.md @@ -0,0 +1,3213 @@ +# 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" +``` + +# 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-smol/packages/effect/LICENSE b/.repos/effect-smol/packages/effect/LICENSE new file mode 100644 index 00000000000..be1f5c14c7b --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/MCP.md b/.repos/effect-smol/packages/effect/MCP.md new file mode 100644 index 00000000000..f44c7e5d018 --- /dev/null +++ b/.repos/effect-smol/packages/effect/MCP.md @@ -0,0 +1,372 @@ +## 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, NodeSink, NodeStream } from "@effect/platform-node" +import { Effect, Layer } from "effect" +import { Logger } from "effect" +import { Schema } from "effect/schema" +import { McpServer, Tool, Toolkit } from "effect/unstable/ai" + +// Define tools +const GreetTool = Tool.make("GreetTool", { + description: "Generate a greeting message", + parameters: { + 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: { + 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", "details"]) + }, + 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", + stdin: NodeStream.stdin, + stdout: NodeSink.stdout + }) + ), + Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })])) +) + +// Run the server +Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) +``` diff --git a/.repos/effect-smol/packages/effect/OPTIC.md b/.repos/effect-smol/packages/effect/OPTIC.md new file mode 100644 index 00000000000..c424778d099 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/README.md b/.repos/effect-smol/packages/effect/README.md new file mode 100644 index 00000000000..513ae19cc21 --- /dev/null +++ b/.repos/effect-smol/packages/effect/README.md @@ -0,0 +1,53 @@ +# `effect` Core Package + +The `effect` package is the heart of the Effect framework, providing robust primitives for managing side effects, ensuring type safety, and supporting concurrency in your TypeScript applications. + +## Requirements + +- **TypeScript 5.4 or Newer:** + Ensure you are using a compatible TypeScript version. + +- **Strict Type-Checking:** + The `strict` flag must be enabled in your `tsconfig.json`. For example: + + ```json + { + "compilerOptions": { + "strict": true + // ...other options + } + } + ``` + +## Installation + +Install the core package using your preferred package manager. For example, with npm: + +```bash +npm install effect +``` + +## Documentation + +- **Website:** + For detailed information and usage examples, visit the [Effect website](https://www.effect.website/). + +- **API Reference:** + For a complete API reference of the core package `effect`, see the [Effect API documentation](https://effect-ts.github.io/effect/). + +## Overview of Effect Modules + +The `effect` package provides a collection of modules designed for functional programming in TypeScript. Below is a brief overview of the core modules: + +| Module | Description | +| -------- | -------------------------------------------------------------------------------------------------------------------------- | +| Effect | The core abstraction for managing side effects, concurrency, and error handling in a structured way. | +| Context | A lightweight dependency injection mechanism that enables passing services through computations without direct references. | +| Layer | A system for managing dependencies, allowing for modular and composable resource allocation. | +| Fiber | Lightweight virtual threads with resource-safe cancellation capabilities, enabling many features in Effect. | +| Stream | A powerful abstraction for handling asynchronous, event-driven data processing. | +| Schedule | A module for defining retry and repeat policies with composable schedules. | +| Scope | Manages the lifecycle of resources, ensuring proper acquisition and release. | +| Schema | A powerful library for defining, validating, and transforming structured data with type-safe encoding and decoding. | + +For a comparison between `effect/Schema` and `zod`, see [Schema vs Zod](https://github.com/Effect-TS/effect/tree/main/packages/effect/schema-vs-zod.md). diff --git a/.repos/effect-smol/packages/effect/SCHEMA.md b/.repos/effect-smol/packages/effect/SCHEMA.md new file mode 100644 index 00000000000..a43f05e4df4 --- /dev/null +++ b/.repos/effect-smol/packages/effect/SCHEMA.md @@ -0,0 +1,7129 @@ +# 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. + +**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 value with a length of at least 1, got "" + at [0]))])) +*/ +``` + +### 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). Every key must satisfy a key schema, and every value must satisfy a value schema. + +### Key Transformations + +`Schema.Record` supports transforming keys during decoding and encoding. This can be useful when working with different naming conventions. + +**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, { + key: { + 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: "a" }))) +// Failure(Cause([Fail(SchemaError(Expected an integer, got 1.1 +// at ["1.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 a non-throwing alternative, use `Schema.makeOption` (or `SchemaParser.makeOption`), which returns `Option.Some` on success and `Option.None` on failure. + +```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.String + +const To = Schema.Number + +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 equality. + +## 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. +- The generated class automatically implements `Equal` so structural equality works out of the box. +- 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** add `Equal` or prototype-based features. 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 values that are not strings, use `Schema.toCodecStringTree` with the `keepDeclarations: true` option. This serializer preserves values such as numbers and `Blob` objects when compatible with the schema. + +**Example** (Parsing non-string values) + +```ts +import { Schema } from "effect" + +const schema = Schema.fromFormData( + Schema.toCodecStringTree( + Schema.Struct({ + a: Schema.Int + }), + { keepDeclarations: true } + ) +) + +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` with the `keepDeclarations: true` option. This serializer preserves values such as numbers or declarations 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 + }), + { keepDeclarations: true } + ) +) + +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' ] +``` + +#### keepDeclarations: true + +The `keepDeclarations: true` option behaves like the StringTree codec, but it does **not** convert declarations without a `toCodecJson` annotation to `undefined`. Instead, it keeps them as they are. + +This is usefult for example when you encode a schema to a `FormData` format and you want to preserve `Blob` values. + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.instanceOf(URL), + b: Schema.Number +}) + +const stringTree = Schema.toCodecStringTree(schema, { keepDeclarations: true }) + +console.log( + Schema.encodeUnknownSync(stringTree)({ + a: new URL("https://effect.website"), + b: 1 + }) +) +// { a: URL("https://effect.website"), b: '1' } +``` + +### 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 testing checks your code against many randomly generated inputs. An Arbitrary is a generator that produces random values matching your schema. Schema can derive an Arbitrary automatically, so you do not need to write generators by hand. + +#### Basic Conversion + +You can convert any non-declaration, non-`never` schema to a Fast-Check `Arbitrary`. + +**Example** (Tuple schema to Fast-Check Arbitrary) + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +// Build a tuple schema: [string, number] +const schema = Schema.Tuple([Schema.String, Schema.Number]) + +// Create Arbitrary +const arb = Schema.toArbitrary(schema) + +// Sample 10 values from the arbitrary +console.log(FastCheck.sample(arb, 10)) +/* +Example Output: +[ + [ '', 0 ], + [ ' /pABDyx+4', -5.147705743113717e+24 ], + [ 'Sw\\', -163415396714545150 ], + [ 'h', 484085596160000 ], + [ 'bind-+$__p', -2.802596928649634e-44 ], + [ 'ref', 3.402820424023848e+38 ], + [ ' <', 4.2045513795681537e-22 ], + [ '!n', 5.894371773718808e+34 ], + [ '&x~', 8580584439808 ], + [ '(# x@', 1.97658453148482e-36 ] +] +*/ +``` + +If you want to avoid bundling Fast-Check automatically, use `makeLazy`. + +**Example** (Lazy creation to control bundling) + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +// Create a factory that needs FastCheck passed in at call time +const lazyArb = Schema.toArbitraryLazy(Schema.String) + +// Later, provide FastCheck (and an optional context) to get the Arbitrary +const arb = lazyArb(FastCheck, {}) // same result as make(...) +``` + +Under the hood, the library traverses the schema AST and, for each node: + +- Emits constants (`null`, `undefined`) +- Maps primitives: `fc.boolean()`, `fc.integer()`, `fc.string()`, `fc.bigInt()` +- Builds tuples via `fc.tuple(...)` with support for optional/rest elements +- Builds structs/records via `fc.record(...)`, including index signatures +- Builds unions via `fc.oneof(...)` +- Handles template literals via `fc.stringMatching(...)` +- Handles recursion (`Schema.suspend`) with depth-limited `fc.oneof(...)` + +It also collects any `.check(...)` filters and applies them to the result via `.filter(...)`. + +#### Adding support for Custom Types + +For a custom type, provide an `arbitrary` annotation to teach the generator how to build values. + +**Example** (Custom Arbitrary for `URL`) + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +const URL = Schema.instanceOf(globalThis.URL, { + title: "URL", + arbitrary: + // Build a URL by first generating a valid web URL string with Fast-Check + () => (fc) => fc.webUrl().map((s) => new globalThis.URL(s)) +}) + +console.log(FastCheck.sample(Schema.toArbitrary(URL), 3)) +/* +Example Output: +[ + new URL('http://g2v.7wk9w96penc.sek/'), + new URL('http://jfeilqoq-ee5.zeenw6cvv.ox'), + new URL('https://g0iubr-ks.rz00c8.fn') +] +*/ +``` + +#### Overriding the default generated Arbitrary + +You can adjust the generated Arbitrary by adding an `arbitrary` annotation. + +```ts +interface Context { + /** + * This flag is set to `true` when the current schema is a suspend. The goal + * is to avoid infinite recursion when generating arbitrary values for + * suspends, so implementations should try to avoid excessive recursion. + */ + readonly isSuspend?: boolean | undefined + readonly constraints?: Annotation.Constraints["constraints"] | undefined +} + +export interface ToArbitrary> { + ( + /* Arbitraries for any type parameters of the schema (if present) */ + typeParameters: { readonly [K in keyof TypeParameters]: FastCheck.Arbitrary } + ): (fc: typeof FastCheck, context: Context) => FastCheck.Arbitrary +} +``` + +**Example** (Override number generator range) + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +// Default number schema (no override) +console.log(FastCheck.sample(Schema.toArbitrary(Schema.Number), 3)) +/* +Example Output: +[ + 1.401298464324817e-44, + 1.1210387714598537e-44, + -3.4028234663852886e+38 +] +*/ + +// Add an override to restrict numbers to integers 10..20 +const schema = Schema.Number.annotate({ + toArbitrary: () => (fc) => fc.integer({ min: 10, max: 20 }) // custom generator +}) + +console.log(FastCheck.sample(Schema.toArbitrary(schema), 3)) +/* +Example Output: +[ 12, 12, 18 ] +*/ +``` + +#### Adding support for custom filters + +Filters created with `.check(...)` can include Arbitrary hints so generators respect the same constraints. + +**Example** (Declare Arbitrary constraints for a custom `nonEmpty` filter) + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +// A reusable 'isNonEmpty' filter for strings and arrays +const isNonEmpty = Schema.makeFilter((s: string) => s.length > 0, { + arbitraryConstraint: { + string: { + minLength: 1 + }, + array: { + minLength: 1 + } + } +}) + +const schema = Schema.String.check(isNonEmpty) + +console.log(FastCheck.sample(Schema.toArbitrary(schema), 3)) +/* +Example Output: +[ 'R|I6', 'q#" Z', 'qc= f' ] +*/ +``` + +#### Integration with synthetic data generation tools + +You can integrate `@faker-js/faker` by adding an `arbitrary` override to your schemas. The helper below ties Faker's randomness to Fast-Check's RNG so samples are reproducible and shrink well. + +**Example** (Faker-powered override tied to Fast-Check's RNG) + +```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 `arbitrary` override. + * The seed comes from Fast-Check so data is reproducible and shrinks correctly. + */ +function fake( + gen: (f: typeof faker, ctx: Schema.Annotations.ToArbitrary.Context) => A +): Schema.Annotations.ToArbitrary.Declaration { + return () => (fc, ctx) => + fc.nat().map((seed) => { + faker.seed(seed) + return gen(faker, ctx) + }) +} + +/** Leaf fields use Faker through the `arbitrary` override */ +const FirstName = Schema.String.annotate({ + toArbitrary: fake((f) => f.person.firstName()) +}) + +const LastName = Schema.String.annotate({ + toArbitrary: fake((f) => f.person.lastName()) +}) + +const Age = Schema.Int.check(Schema.isBetween({ minimum: 18, maximum: 80 })).annotate({ + toArbitrary: fake((f, ctx) => { + // Use the constraints from the schema to generate a random age + const min = ctx.constraints?.number?.min ?? 0 + const max = ctx.constraints?.number?.max ?? Number.MAX_SAFE_INTEGER + return f.number.int({ min, max }) + }) +}) + +/** Compose leaves with regular Schema combinators */ +const FullName = Schema.Struct({ + firstName: FirstName, + lastName: LastName, + age: Age +}) + +/** Build and sample an Arbitrary for the composed schema */ +console.log(JSON.stringify(FastCheck.sample(Schema.toArbitrary(FullName), 3), null, 2)) +/* +Example Output: +[ + { + "firstName": "Kiana", + "lastName": "Balistreri", + "age": 18 + }, + { + "firstName": "Wendy", + "lastName": "Baumbach", + "age": 51 + }, + { + "firstName": "Kelton", + "lastName": "Kshlerin", + "age": 72 + } +] +*/ +``` + +### 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-smol/packages/effect/benchmark/schema/Enums.ts b/.repos/effect-smol/packages/effect/benchmark/schema/Enums.ts new file mode 100644 index 00000000000..fad7abed0db --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/benchmark/schema/Optic.ts b/.repos/effect-smol/packages/effect/benchmark/schema/Optic.ts new file mode 100644 index 00000000000..0581d5fa332 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/benchmark/schema/array.ts b/.repos/effect-smol/packages/effect/benchmark/schema/array.ts new file mode 100644 index 00000000000..b4596fabe85 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/benchmark/schema/filter.ts b/.repos/effect-smol/packages/effect/benchmark/schema/filter.ts new file mode 100644 index 00000000000..6ff05e83ab8 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/benchmark/schema/object.ts b/.repos/effect-smol/packages/effect/benchmark/schema/object.ts new file mode 100644 index 00000000000..18cb8bdb402 --- /dev/null +++ b/.repos/effect-smol/packages/effect/benchmark/schema/object.ts @@ -0,0 +1,82 @@ +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)' │ '186.46 ± 8.13%' │ '167.00 ± 0.00' │ '5977611 ± 0.01%' │ '5988024 ± 0' │ 5363126 │ +│ 1 │ 'Schema (bad)' │ '314.61 ± 4.26%' │ '250.00 ± 0.00' │ '3722885 ± 0.01%' │ '4000000 ± 0' │ 3178549 │ +│ 2 │ 'Valibot (good)' │ '56.61 ± 4.79%' │ '42.00 ± 0.00' │ '21277611 ± 0.01%' │ '23809524 ± 1' │ 17664248 │ +│ 3 │ 'Valibot (bad)' │ '140.10 ± 3.77%' │ '125.00 ± 0.00' │ '8444864 ± 0.01%' │ '8000000 ± 0' │ 7137707 │ +│ 4 │ 'Arktype (good)' │ '23.61 ± 0.69%' │ '41.00 ± 1.00' │ '32158704 ± 0.01%' │ '24390244 ± 580720' │ 42362846 │ +│ 5 │ 'Arktype (bad)' │ '1657.4 ± 3.09%' │ '1583.0 ± 41.00' │ '632160 ± 0.02%' │ '631712 ± 16796' │ 603347 │ +│ 6 │ 'Zod (good)' │ '40.67 ± 4.22%' │ '42.00 ± 0.00' │ '23805944 ± 0.00%' │ '23809524 ± 0' │ 24587718 │ +│ 7 │ 'Zod (bad)' │ '4970.6 ± 1.58%' │ '4791.0 ± 83.00' │ '207106 ± 0.03%' │ '208725 ± 3635' │ 201183 │ +└─────────┴──────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ +*/ + +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 = 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-smol/packages/effect/benchmark/schema/tagged-union.ts b/.repos/effect-smol/packages/effect/benchmark/schema/tagged-union.ts new file mode 100644 index 00000000000..2250ea4c07d --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/benchmark/schema/transformation.ts b/.repos/effect-smol/packages/effect/benchmark/schema/transformation.ts new file mode 100644 index 00000000000..40cae620f07 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/benchmark/stream/splitLines.ts b/.repos/effect-smol/packages/effect/benchmark/stream/splitLines.ts new file mode 100644 index 00000000000..c85569e2c4d --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/docgen.json b/.repos/effect-smol/packages/effect/docgen.json new file mode 100644 index 00000000000..99d82864a57 --- /dev/null +++ b/.repos/effect-smol/packages/effect/docgen.json @@ -0,0 +1,38 @@ +{ + "$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-smol/tree/main/packages/effect/src/", + "tscExecutable": "tsgo", + "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-smol/packages/effect/package.json b/.repos/effect-smol/packages/effect/package.json new file mode 100644 index 00000000000..c033fbfd7d1 --- /dev/null +++ b/.repos/effect-smol/packages/effect/package.json @@ -0,0 +1,127 @@ +{ + "name": "effect", + "type": "module", + "version": "4.0.0-beta.73", + "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-smol.git", + "directory": "packages/effect" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect-smol/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", + "build:tsgo": "tsgo -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": "^25.7.0", + "ajv": "^8.20.0", + "arktype": "^2.2.0", + "ast-types": "^0.14.2", + "immer": "^11.1.8", + "tinybench": "^6.0.1", + "valibot": "^1.4.0" + }, + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.8.0", + "find-my-way-ts": "^0.1.6", + "ini": "^7.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^2.0.1", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^14.0.0", + "yaml": "^2.9.0" + } +} diff --git a/.repos/effect-smol/packages/effect/src/Array.ts b/.repos/effect-smol/packages/effect/src/Array.ts new file mode 100644 index 00000000000..8eccc47534b --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Array.ts @@ -0,0 +1,4923 @@ +/** + * The `Array` module provides functional operations for JavaScript arrays, + * readonly arrays, and arrays that are known to contain at least one element. + * Operations that transform, reorder, or update collections allocate new arrays + * instead of mutating their inputs, while preserving useful type information + * such as non-emptiness when the operation can prove it. + * + * **Mental model** + * + * - A regular `Array
` is still the built-in JavaScript array type; this + * module supplies functional constructors, combinators, searches, folds, + * grouping, sorting, and set-like operations around it. + * - {@link NonEmptyReadonlyArray} and {@link NonEmptyArray} encode + * non-emptiness at the type level. APIs with `NonEmpty` in the name can avoid + * `Option` because an element is guaranteed to exist. + * - Most functions are dual. You can call them data-first, such as + * `Array.map(values, f)`, or data-last in a pipeline, such as + * `pipe(values, Array.map(f))`. + * - Safe element access returns {@link Option}; unsafe or `NonEmpty` variants + * are for code that already has a proof an index or element exists. + * - Set-like operations such as {@link union}, {@link intersection}, and + * {@link difference} use the {@link Equal} protocol by default. Use the + * `*With` variants when equality is domain-specific. + * + * **Common tasks** + * + * - Create arrays with {@link make}, {@link of}, {@link empty}, + * {@link fromIterable}, {@link range}, {@link makeBy}, {@link replicate}, and + * {@link unfold}. + * - Access edges or indexes with {@link head}, {@link last}, {@link get}, + * {@link tail}, and {@link init}. + * - Transform and flatten with {@link map}, {@link flatMap}, and + * {@link flatten}. + * - Keep, split, or deduplicate values with {@link filter}, {@link partition}, + * {@link dedupe}, and {@link dedupeAdjacent}. + * - Combine collections with {@link append}, {@link prepend}, {@link appendAll}, + * {@link prependAll}, {@link zip}, and {@link cartesian}. + * - Chunk, window, and slice with {@link splitAt}, {@link chunksOf}, + * {@link span}, and {@link window}. + * - Sort with {@link sort}, {@link sortWith}, and {@link sortBy}. + * - Fold or aggregate with {@link reduce}, {@link scan}, {@link join}, and + * {@link countBy}. + * - Match empty and non-empty cases with {@link match}, {@link matchLeft}, and + * {@link matchRight}. + * + * **Gotchas** + * + * - {@link fromIterable} returns the original array reference when the input is + * already an array. Use {@link copy} when you need a fresh shallow copy. + * - {@link makeBy}, {@link range}, and {@link replicate} always return + * non-empty arrays. `range(start, end)` is inclusive and returns `[start]` + * when `start > end`. + * - Functions returning {@link Option}, such as {@link head} and + * {@link findFirst}, return `Option.none()` for empty inputs instead of + * throwing. + * - `NonEmpty` return types describe what the function can prove, not what may + * happen for a particular runtime value after filtering. + * + * **Example** (Filtering and transforming) + * + * ```ts + * import { Array, Option, pipe } from "effect" + * + * const numbers = [1, 2, 3, 4, 5] + * + * const doubledEvens = pipe( + * numbers, + * Array.filter((n) => n % 2 === 0), + * Array.map((n) => n * 2) + * ) + * + * console.log(doubledEvens) + * // [4, 8] + * + * const first = Array.head(doubledEvens) + * console.log(Option.getOrElse(first, () => 0)) + * // 4 + * ``` + * + * @see {@link make} — create a non-empty array from elements + * @see {@link map} — transform each element + * @see {@link filter} — keep elements matching a predicate + * @see {@link reduce} — fold an array to a single value + * + * @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** (Using 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 you use this type when you need to ensure non-emptiness 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 have literal values and want a typed non-empty array. + * - The element type is inferred as the union of all arguments. + * - Always returns a `NonEmptyArray` since at least one argument is required. + * + * **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 and will fill it imperatively. + * - Elements are typed as `A | undefined` since slots are empty. + * - Prefer {@link makeBy} when you can compute each element from its index. + * + * **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 an array whose values depend on the index. + * - `n` is normalized to an integer >= 1 — always returns at least one element. + * - Dual: `Array.makeBy(5, f)` or `pipe(5, Array.makeBy(f))`. + * + * **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 sequence of consecutive integers. + * - If `start > end`, returns `[start]`. + * - Always returns a `NonEmptyArray`. + * + * **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 multiple copies of the same value. + * - `n` is normalized to an integer >= 1 — always returns at least one element. + * - Dual: `Array.replicate("a", 3)` or `pipe("a", Array.replicate(3))`. + * + * **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, returns it **by reference** (no copy). + * - Otherwise, creates a new array from the iterable. + * - Use {@link 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, returns it by reference. + * - If the input is a single value, wraps it in a one-element array. + * - 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. + * - Returns an empty array for an empty record. + * + * **Example** (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** (Option to 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 has elements. + * - `onNonEmpty` receives a `NonEmptyReadonlyArray`. + * - Dual: data-first or data-last. + * + * **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 to pattern-match when you need the first element and remaining elements as + * separate values. + * + * **Details** + * + * `onNonEmpty` receives `(head, tail)` where `tail` is the rest of the array. + * + * **Example** (Head and tail destructuring) + * + * ```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 to pattern-match when you need all but the last element and the last element + * as separate values. + * + * **Details** + * + * `onNonEmpty` receives `(init, last)` where `init` is everything but the last element. + * + * **Example** (Init and last destructuring) + * + * ```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 to add a single element at the start of an iterable and get a `NonEmptyArray`. + * + * **Details** + * + * - Always returns a non-empty array. + * + * **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 concatenating + * @since 2.0.0 + */ +export const prepend: { + (head: B): (self: Iterable) => NonEmptyArray + (self: Iterable, head: B): NonEmptyArray +} = dual(2, (self: Iterable, head: B): NonEmptyArray => [head, ...self]) + +/** + * Prepends 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 concatenating + * @since 2.0.0 + */ +export const prependAll: { + , T extends Iterable>( + that: T + ): (self: S) => ReadonlyArray.OrNonEmpty | ReadonlyArray.Infer> + (self: Iterable, that: NonEmptyReadonlyArray): NonEmptyArray + (self: NonEmptyReadonlyArray, that: Iterable): NonEmptyArray + (self: Iterable, that: Iterable): Array +} = dual( + 2, + (self: Iterable, that: Iterable): Array => fromIterable(that).concat(fromIterable(self)) +) + +/** + * Adds a single element to the end of an iterable, returning a `NonEmptyArray`. + * + * **When to use** + * + * Use to add one element to the end of an iterable and get a new + * `NonEmptyArray`. + * + * **Details** + * + * - Always returns a non-empty array. + * + * **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 concatenating + * @since 2.0.0 + */ +export const append: { + (last: B): (self: Iterable) => NonEmptyArray + (self: Iterable, last: B): NonEmptyArray +} = dual(2, (self: Iterable, last: B): Array => [...self, last]) + +/** + * Concatenates two 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 concatenating + * @since 2.0.0 + */ +export const appendAll: { + , T extends Iterable>( + that: T + ): (self: S) => ReadonlyArray.OrNonEmpty | ReadonlyArray.Infer> + (self: Iterable, that: NonEmptyReadonlyArray): NonEmptyArray + (self: NonEmptyReadonlyArray, that: Iterable): NonEmptyArray + (self: Iterable, that: Iterable): Array +} = dual( + 2, + (self: Iterable, that: Iterable): Array => fromIterable(self).concat(fromIterable(that)) +) + +/** + * 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` (starts with the initial value). + * - Always returns a `NonEmptyArray` because the initial value is included. + * - Use {@link 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` (ends with the initial value). + * - Always returns a `NonEmptyArray`. + * + * **Example** (Reverse running totals) + * + * ```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`. + * - 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`. + * + * **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`. + * + * **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. + * - Never throws. + * + * **Example** (Safe index access) + * + * ```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 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 {@link get} for safe access. + * + * **Example** (Unsafe index access) + * + * ```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]`. + * - 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 to split a non-empty array from the end when you need both the elements + * before the last element and the last element. + * + * **Details** + * + * - Returns a tuple `[init, last]`. + * - 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)`. + * - Returns `Option.none()` for empty inputs. + * + * **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)`. + * - Returns `Option.none()` for empty inputs. + * + * **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 of elements while a function can + * successfully extract or transform them, stopping at the first element + * that produces a failure result. + * + * **Details** + * + * - The filter receives `(element, index)`. + * - 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 to split an iterable into the longest prefix that satisfies a predicate + * and the elements after that prefix when you need both parts. + * + * **Details** + * + * - Equivalent to `[takeWhile(pred), dropWhile(pred)]` but more efficient + * (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]`. + * - Returns a copy of the full array when `n <= 0`. + * + * **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 dropping a prefix requires computing a `Result` per element instead + * of a simple boolean predicate. + * + * **Details** + * + * - The filter receives `(element, index)`. + * - Returns 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. + * - Returns `Option.none()` if no element matches. + * + * **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. + * - Returns `Option.none()` if no element matches. + * + * **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: `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 the current 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 to remove a single element at a known index. + * + * **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 the order of elements 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 {@link sortWith} to sort by a derived key, or {@link 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 values need to be sorted by a derived key, such as a string length + * or object field, while the output should keep 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** + * + * - Data-last only (returns a function). + * - Preserves `NonEmptyArray` in the return type. + * + * **Example** (Multi-key sorting) + * + * ```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 to pair corresponding elements from two iterables when you need simple pairs without a custom combiner. + * + * **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. + * + * **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** + * + * - Preserves `NonEmptyArray` in the return type. + * - An empty input produces 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. + * + * **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. + * + * **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. + * + * **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. + * - Preserves `NonEmptyArray` in the return type. + * - Returns a copy for empty arrays or when the normalized rotation is `0`. + * + * **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(self, 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** (Custom equality check) + * + * ```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 membership with 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 the grouping logic is custom and each step needs to return both a + * value and the remaining input. + * + * **Details** + * + * - The function receives a `NonEmptyReadonlyArray` and returns + * `[value, rest]`. + * - Continues until the remaining array is empty. + * - Useful for custom splitting/grouping logic. + * + * **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` (all elements in the second array). + * - `n` 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). + * + * **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 to split an array at a condition boundary when you know which element marks the transition point. + * + * **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** + * + * - Preserves `NonEmptyArray` in the return type. + * - Useful when you need a distinct reference (e.g. before mutating). + * + * **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 non-overlapping chunks with a maximum chunk + * size. + * + * **Details** + * + * - `chunksOf(n)([])` is `[]`, not `[[]]`. + * - Each chunk is a `NonEmptyArray`. + * - Preserves `NonEmptyArray` in the outer return type. + * + * **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** (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 a non-empty array is already arranged so matching elements are + * adjacent and you need a custom equivalence function. + * + * **Details** + * + * - Only groups **adjacent** elements — 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 equal values are already adjacent and Effect's default equality is + * the right comparison. + * + * **Details** + * + * - Only groups **adjacent** elements. + * + * **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 {@link group}/{@link groupWith}, elements do not need to be + * adjacent to be grouped together. + * - 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** (Union 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** (Array union) + * + * ```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 keeping only values present in both arrays and equality must be + * defined by a custom comparator, such as matching objects by id. + * + * **Example** (Intersection 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. + * + * **Example** (Array intersection) + * + * ```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 keeping only values from the first array and equality must be + * defined by a custom comparator, such as matching objects by id. + * + * **Example** (Difference 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** (Array difference) + * + * ```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)`. + * - Preserves `NonEmptyArray` in the return type. + * + * **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 element to zero or more values and concatenate the results in + * one pass. + * + * **Details** + * + * - The function receives `(element, index)`. + * - Returns `NonEmptyArray` when both input and mapped arrays are non-empty. + * + * **Example** (FlatMapping 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 `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 to collect only failure values from `Result` values while discarding + * successes. + * + * **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 to collect only success values from `Result` values while discarding + * failures. + * + * **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 transform elements with a `Result`-returning filter while discarding + * failures. + * + * **Details** + * + * - The filter receives `(element, index)`. + * - Failures are discarded. + * + * **Example** (Filter and transform) + * + * ```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 keep original elements that satisfy a boolean predicate or refinement. + * + * **Details** + * + * - The predicate receives `(element, index)`. + * - Supports refinements 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 evaluate each element with a `Result`-returning filter and keep 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 existing `Result` values into failure and success arrays. + * + * **Details** + * + * - Returns `[failures, successes]`. + * - 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 folding order matters and values must be combined from right to + * left. + * + * **Details** + * + * - The function receives `(accumulator, element, index)`. + * + * **Example** (Right-to-left fold) + * + * ```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** (Conditional wrapping) + * + * ```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 `[]`. + * + * **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** (Nullable to 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 mapping and filtering in one step, where the mapper can return + * `null` or `undefined` to skip elements. + * + * **Example** (FlatMapping with nullable) + * + * ```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]`. + * + * **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 that all elements satisfy 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 a computation depends on every suffix of an array, such as + * cumulative aggregations from each position. + * + * **Details** + * + * - For index `i`, the function receives `self.slice(i)`. + * + * **Example** (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 mapping needs state threaded through each element and the final state + * is also needed. + * + * **Details** + * + * - Combines `map` and `reduce` in a single pass. + * - The callback receives the current state, element, and index, and returns `[nextState, mappedValue]`. + * - Returns `[finalState, mappedArray]`. + * - Dual: 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`. + * - Result length is `self.length * that.length`. + * - Order: iterates `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`. + * - Result length is `self.length * that.length`. + * + * **Example** (All pairs of 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 begin a pipeline with `Do`, then 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** (Array comprehension 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. + * - Equivalent to `flatMap` + 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 }))`. + * - Alternative to starting with `Do` + `bind`; useful 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. + * + * **Details** + * + * - Unlike {@link bind}, the callback returns a single value (not an array), so no cartesian product occurs. + * - Useful 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 to count how many elements satisfy a predicate when you only need the + * number of matches instead of the matching elements. + * + * **Details** + * + * - The predicate receives both the element and its index. + * - Returns `0` for an empty iterable. + * + * **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-smol/packages/effect/src/BigDecimal.ts b/.repos/effect-smol/packages/effect/src/BigDecimal.ts new file mode 100644 index 00000000000..4452b39f83a --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/BigDecimal.ts @@ -0,0 +1,1990 @@ +/** + * Decimal arithmetic backed by an unscaled `bigint` and a decimal scale. + * + * `BigDecimal` is useful when values must keep their decimal representation + * instead of inheriting JavaScript's binary floating-point rounding, such as + * money, quantities, measurements, or protocol values exchanged as decimal + * strings. The module includes constructors, parsers, arithmetic operations, + * comparisons, rounding helpers, and string formatting. + * + * **Mental model** + * + * - A `BigDecimal` stores `value * 10^-scale`; `make(12345n, 2)` represents + * `123.45` + * - Values are immutable; arithmetic returns new `BigDecimal` values + * - Equivalent values can have different internal scales, so equality and + * ordering normalize trailing zeros + * - Division can produce repeating decimals, so {@link divide} and + * {@link divideUnsafe} use the module's default division precision + * + * **Common tasks** + * + * - Construct values: {@link make}, {@link fromBigInt}, {@link fromString}, + * {@link fromNumber} + * - Render values: {@link format}, {@link toExponential}, + * {@link toNumberUnsafe} + * - Do arithmetic: {@link sum}, {@link subtract}, {@link multiply}, + * {@link divide}, {@link remainder}, {@link negate}, {@link abs} + * - Compare and constrain values: {@link equals}, {@link Order}, + * {@link isLessThan}, {@link between}, {@link clamp}, {@link min}, + * {@link max} + * - Adjust decimal places: {@link scale}, {@link round}, {@link truncate}, + * {@link ceil}, {@link floor} + * + * **Gotchas** + * + * - Prefer {@link fromString} or {@link fromBigInt} for external decimal data. + * {@link fromNumber} can only preserve the decimal spelling of a finite + * JavaScript number after any binary floating-point precision has already + * been lost. + * - {@link divide} and {@link remainder} return `Option.none()` for division + * by zero; {@link divideUnsafe} and {@link remainderUnsafe} throw instead. + * - Parsed scales must fit in JavaScript's safe integer range. + * + * **Example** (Decimal arithmetic) + * + * ```ts + * import { BigDecimal } from "effect" + * + * const subtotal = BigDecimal.multiply( + * BigDecimal.fromStringUnsafe("19.99"), + * BigDecimal.fromBigInt(3n) + * ) + * const total = BigDecimal.sum(subtotal, BigDecimal.fromStringUnsafe("1.50")) + * + * console.log(BigDecimal.format(total)) // "61.47" + * ``` + * + * @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 to add two `BigDecimal` values. + * + * **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 to sum all `BigDecimal` values in an iterable. + * + * **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 the decimal quotient and 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 sorting or comparing 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, throwing for division by zero. + * + * **When to use** + * + * Use when you need the decimal remainder and the divisor is known to be + * non-zero, so division by zero should be a thrown exception. + * + * **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`, throwing for non-finite input. + * + * **When to use** + * + * Use when a finite JavaScript number must become a `BigDecimal` and invalid + * input should throw. + * + * **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 decimal text is expected to be valid and parse errors should 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 an interop boundary requires a JavaScript number and can tolerate + * precision loss. + * + * **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 to remove digits beyond a requested scale by rounding toward zero. + * + * **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-smol/packages/effect/src/BigInt.ts b/.repos/effect-smol/packages/effect/src/BigInt.ts new file mode 100644 index 00000000000..e3dea271b6d --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/BigInt.ts @@ -0,0 +1,1088 @@ +/** + * Tools for working with JavaScript `bigint` values in Effect code. The module + * includes arithmetic, comparisons, range checks, safe conversions, integer + * square roots, aggregation helpers, and `Order`, `Equivalence`, `Reducer`, and + * `Combiner` instances for APIs that consume those abstractions. + * + * Reach for `BigInt` when values may exceed JavaScript's safe `number` integer + * range, when conversions should make failure explicit with `Option`, or when + * Effect collection APIs need bigint-specific ordering or combining behavior. + * + * **Mental model** + * + * - Values are native JavaScript `bigint`s; the module does not introduce a + * wrapper type. + * - Binary operations such as {@link sum}, {@link multiply}, {@link subtract}, + * {@link divide}, {@link min}, and {@link max} are dual and work in + * data-first or data-last style. + * - Safe operations return `Option`, including {@link divide}, {@link sqrt}, + * {@link fromString}, {@link fromNumber}, and {@link toNumber}. + * - Unsafe or native operations keep JavaScript behavior, including thrown + * errors from {@link BigInt}, {@link divideUnsafe}, {@link sqrtUnsafe}, and + * {@link remainder} for invalid inputs. + * + * **Common tasks** + * + * - Check and construct values: {@link isBigInt}, {@link BigInt}, + * {@link fromString}, {@link fromNumber}, {@link toNumber} + * - Do arithmetic: {@link sum}, {@link multiply}, {@link subtract}, + * {@link divide}, {@link divideUnsafe}, {@link remainder}, {@link increment}, + * {@link decrement} + * - Compare and bound values: {@link Order}, {@link Equivalence}, + * {@link isLessThan}, {@link isLessThanOrEqualTo}, {@link isGreaterThan}, + * {@link isGreaterThanOrEqualTo}, {@link between}, {@link clamp}, + * {@link min}, {@link max} + * - Work with signs and number theory: {@link sign}, {@link abs}, {@link gcd}, + * {@link lcm}, {@link sqrt}, {@link sqrtUnsafe} + * - Aggregate many values: {@link sumAll}, {@link multiplyAll}, + * {@link ReducerSum}, {@link ReducerMultiply}, {@link CombinerMax}, + * {@link CombinerMin} + * + * **Gotchas** + * + * - JavaScript does not allow mixing `number` and `bigint` in arithmetic. Use + * {@link fromNumber} and {@link toNumber} when crossing that boundary. + * - JavaScript `bigint` division truncates toward zero, and {@link remainder} + * follows JavaScript `%` semantics. + * - The native {@link BigInt} constructor follows JavaScript coercion rules and + * may throw. Use {@link fromString} or {@link fromNumber} when failed + * conversion should be represented as `Option.none()`. + * + * **Quickstart** + * + * **Example** (Safe arithmetic and conversion) + * + * ```ts + * import { BigInt } from "effect" + * + * const total = BigInt.sumAll([10n, 20n, 30n]) + * const average = BigInt.divide(total, 3n) + * const bounded = BigInt.clamp(total, { minimum: 0n, maximum: 50n }) + * + * console.log(total) // 60n + * console.log(average) // Option.some(20n) + * console.log(bounded) // 50n + * console.log(BigInt.fromString("not an integer")) // Option.none() + * ``` + * + * @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 to add two `bigint` values. + * + * **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 when 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 sorting or comparing 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` value, the function returns the `minimum` value. + * - If the `bigint` is greater than the `maximum` value, the function returns the `maximum` value. + * - Otherwise, it returns the original `bigint`. + * + * **Example** (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 the input is known to be non-negative and invalid input should throw. + * + * **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 to sum all `bigint` values in an iterable. + * + * **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 to compute the JavaScript `%` remainder for two `bigint` values. + * + * **Details** + * + * The result follows JavaScript `%` semantics, including the sign of the + * dividend. + * + * **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-smol/packages/effect/src/Boolean.ts b/.repos/effect-smol/packages/effect/src/Boolean.ts new file mode 100644 index 00000000000..03458f9224f --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Boolean.ts @@ -0,0 +1,528 @@ +/** + * Operations for working with TypeScript `boolean` values in expression-oriented + * code. Use this module to narrow unknown input, branch without inline + * conditionals, combine boolean flags with named logical operators, and reduce + * boolean collections. + * + * **Mental model** + * + * Boolean values are plain JavaScript booleans. The module adds a small + * vocabulary around them: {@link match} for choosing between two lazy branches, + * logical combinators such as {@link and}, {@link or}, {@link xor}, and + * {@link implies}, and collection helpers such as {@link every} and + * {@link some}. The exported {@link Order} sorts `false` before `true`, and + * {@link Equivalence} compares booleans with strict equality. + * + * **Common tasks** + * + * - Coerce or narrow input: {@link Boolean}, {@link isBoolean} + * - Branch on a boolean: {@link match} + * - Invert and combine flags: {@link not}, {@link and}, {@link or}, + * {@link nand}, {@link nor}, {@link xor}, {@link eqv}, {@link implies} + * - Check boolean collections: {@link every}, {@link some} + * - Pass boolean instances to generic APIs: {@link Order}, {@link Equivalence}, + * {@link ReducerAnd}, {@link ReducerOr} + * + * **Gotchas** + * + * - {@link Boolean} is the native JavaScript constructor. It follows truthiness + * rules, so values such as `"false"` and `[]` coerce to `true`. + * - {@link every} returns `true` for an empty iterable, matching logical AND. + * {@link some} returns `false` for an empty iterable, matching logical OR. + * - {@link ReducerAnd} and {@link ReducerOr} are for APIs that consume a + * `Reducer`; use {@link every} or {@link some} when you want direct iterable + * checks. + * + * **Quickstart** + * + * **Example** (Combining validation flags) + * + * ```ts + * import { Boolean } from "effect" + * + * const hasName = true + * const hasEmail = false + * + * const isComplete = Boolean.and(hasName, hasEmail) + * console.log(isComplete) // false + * + * const message = Boolean.match(isComplete, { + * onFalse: () => "missing fields", + * onTrue: () => "ready" + * }) + * console.log(message) // "missing fields" + * ``` + * + * @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 sorting or comparing 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 boolean 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 boolean 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 utils + * @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 utils + * @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-smol/packages/effect/src/Brand.ts b/.repos/effect-smol/packages/effect/src/Brand.ts new file mode 100644 index 00000000000..9ad5d9ea960 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Brand.ts @@ -0,0 +1,360 @@ +/** + * 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. + * + * **Mental model** + * + * - {@link Branded} is an existing value type plus a phantom {@link Brand} + * marker + * - {@link nominal} creates constructors for purely nominal brands and + * performs no runtime validation + * - {@link make} and {@link check} create constructors that validate input + * before returning the branded value + * - A {@link Constructor} can throw, return `Option`, return `Result`, or act + * as a type guard through its `is` method + * - {@link all} combines multiple brand constructors with the same base type so + * a value can carry several brands + * + * **Common tasks** + * + * - Distinguish identifiers with the same primitive representation, such as + * `UserId` and `OrderId` + * - Constrain primitive values after validation, such as positive numbers, + * non-empty strings, or normalized tokens + * - Keep public APIs precise without wrapping values in runtime classes + * + * **Quickstart** + * + * **Example** (Validated identifier) + * + * ```ts + * import { Brand } from "effect" + * + * type UserId = Brand.Branded + * + * const UserId = Brand.make( + * (n) => Number.isInteger(n) && n > 0 || "Expected a positive integer" + * ) + * + * const id = UserId(1) + * ``` + * + * **Gotchas** + * + * - Brands do not change the runtime value; `id` above is still a number at + * runtime + * - {@link nominal} accepts every value of the base type, so use + * {@link make} or {@link check} at trust boundaries that need validation + * + * @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 AST from "./SchemaAST.ts" +import type * as Issue 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 [AST.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: Issue.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: Issue.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. If you also want to perform + * some validation, see {@link make} or {@link check}. + * + * @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. If you + * don't want to perform any validation but only distinguish between two values + * of the same type but with different meanings, see {@link nominal}. + * + * @category constructors + * @since 4.0.0 + */ +export function make>( + filter: (unbranded: Brand.Unbranded) => Schema.FilterOutput +): Constructor { + return check(AST.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 [ + AST.Check>, + ...Array>> + ] +): Constructor { + const result = (input: Brand.Unbranded): Result.Result => { + return Result.mapError(AST.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-smol/packages/effect/src/Cache.ts b/.repos/effect-smol/packages/effect/src/Cache.ts new file mode 100644 index 00000000000..edc59994a3d --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Cache.ts @@ -0,0 +1,1334 @@ +/** + * The `Cache` module provides an effectful, mutable key-value cache for values + * computed by lookup effects. A `Cache` stores lookup exits for + * keys, shares concurrent misses for the same key, and manages entry lifetime + * with capacity limits and optional time-to-live policies. + * + * **Mental model** + * + * - {@link make} and {@link makeWith} create a cache from a lookup function and + * a maximum capacity + * - {@link get} returns a cached value when present, or runs the lookup for a + * missing or expired key + * - Concurrent misses for the same key share one pending lookup and all await + * the same result + * - Successes and failures are both cached as `Exit` values until their entry + * expires or is replaced + * - Entries can live forever, use a fixed TTL, or use a dynamic TTL based on + * the lookup `Exit` + * - Capacity uses access order: reads move entries to the back and overflow + * removes the oldest entries + * + * **Common tasks** + * + * - Create a cache: {@link make}, {@link makeWith} + * - Read values: {@link get}, {@link getOption}, {@link getSuccess} + * - Seed or overwrite values: {@link set} + * - Refresh values: {@link refresh} + * - Remove entries: {@link invalidate}, {@link invalidateWhen}, {@link invalidateAll} + * - Inspect contents: {@link has}, {@link size}, {@link keys}, {@link values}, {@link entries} + * + * **Gotchas** + * + * - {@link getOption} does not run the lookup, but it awaits pending entries + * and fails when the existing entry is a failure + * - {@link getSuccess} returns `Option.none` for missing, expired, pending, or + * failed entries + * - {@link size} may include expired entries until they are observed and removed + * - {@link values} and {@link entries} include only successfully resolved, + * non-expired entries + * - Use `Data` or another `Equal`-compatible key type when keys should compare + * structurally + * + * **See also** + * + * - {@link Duration} for configuring fixed or dynamic time-to-live values + * - {@link Effect} for the lookup effects used to compute cached values + * + * @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** (Using 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-smol/packages/effect/src/Cause.ts b/.repos/effect-smol/packages/effect/src/Cause.ts new file mode 100644 index 00000000000..2bcac7073a6 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Cause.ts @@ -0,0 +1,2007 @@ +/** + * Structured representation of how an Effect can fail. + * + * A `Cause` holds a flat array of `Reason` values, where each reason is one of: + * + * - **Fail** — a typed, expected error `E` (created by `Effect.fail`) + * - **Die** — an untyped defect (`unknown`) from `Effect.die` or uncaught throws + * - **Interrupt** — a fiber interruption, optionally carrying the interrupting fiber's ID + * + * ## Mental model + * + * - A `Cause` is always flat: concurrent and sequential failures are stored together + * in `cause.reasons` (a `ReadonlyArray>`). + * - Each `Reason` carries an `annotations` map with tracing metadata (stack frames, spans). + * - An empty `reasons` array means the computation succeeded or the cause was empty + * ({@link empty}). + * - `Cause` implements `Equal`, so two causes with identical reasons compare as equal. + * + * ## Common tasks + * + * | Intent | API | + * |--------|-----| + * | Create a cause | {@link fail}, {@link die}, {@link interrupt}, {@link fromReasons} | + * | Test for reason types | {@link hasFails}, {@link hasDies}, {@link hasInterrupts} | + * | Extract the first error/defect | {@link findError}, {@link findDefect}, {@link findFail}, {@link findDie} | + * | Iterate over reasons manually | `cause.reasons.filter(Cause.isFailReason)` | + * | Combine two causes | {@link combine} | + * | Transform errors | {@link map} | + * | Collapse to a single thrown value | {@link squash} | + * | Render for logging | {@link pretty}, {@link prettyErrors} | + * | Attach/read tracing metadata | {@link annotate}, {@link annotations}, {@link reasonAnnotations} | + * + * ## Gotchas + * + * - `findError`/`findDefect` return `Result.fail` (not `Option.none`) when no match is + * found. Use {@link findErrorOption} if you need an `Option`. + * - `squash` picks the first `Fail` error, then the first `Die` defect, then falls back + * to a generic "interrupted" / "empty" error. It is lossy — use `prettyErrors` or + * iterate `reasons` directly when you need all failures. + * - The module also exports several built-in error classes (`NoSuchElementError`, + * `TimeoutError`, `IllegalArgumentError`, `ExceededCapacityError`, `UnknownError`) + * and the `Done` completion signal. These all implement `YieldableError` and can be + * yielded directly inside `Effect.gen`. + * + * **Example** (inspecting a concurrent failure) + * + * ```ts + * import { Cause, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const cause = yield* Effect.sandbox( + * Effect.all([ + * Effect.fail("err1"), + * Effect.die("defect"), + * Effect.fail("err2") + * ], { concurrency: "unbounded" }) + * ).pipe(Effect.flip) + * + * const errors = cause.reasons + * .filter(Cause.isFailReason) + * .map((r) => r.error) + * .sort() + * + * const defects = cause.reasons + * .filter(Cause.isDieReason) + * .map((r) => String(r.defect)) + * .sort() + * + * console.log(errors.join(",")) // "err1,err2" + * console.log(defects.join(",")) // "defect" + * }) + * + * Effect.runPromise(program) + * ``` + * + * @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** (runtime type check) + * + * ```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** (runtime type check) + * + * ```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 deciding whether a failure was entirely due to interruption and + * can be silently discarded. + * + * **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 use {@link findError} if you only need the unwrapped error value `E`. + * + * **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 use {@link findFail} if you need the full `Fail` reason (including + * annotations). Use {@link findErrorOption} if you prefer an `Option`. + * + * **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 this is the `Option`-returning variant of {@link findError} for code that + * does not need the original cause returned in a failed `Result`. + * + * **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 use {@link findDefect} if you only need the unwrapped defect value. + * + * **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 use {@link findDie} if you need the full `Die` reason (including + * annotations). + * + * **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 to extract the first interruption reason when you need its 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 this always succeeds. Use {@link filterInterruptors} when you want a + * `Result` that fails with the original cause if there are no `Interrupt` + * reasons. + * + * **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 use {@link interruptors} if you always want a `Set` without `Result` + * wrapping. + * + * **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) => 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** (runtime type check) + * + * ```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) + * + * ```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** (runtime type check) + * + * ```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 the completion signal value itself. Use {@link done} + * when you need an `Effect` that fails with the signal. + * + * @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 use this in effect workflows that 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** (runtime type check) + * + * ```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) + * + * ```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** (runtime type check) + * + * ```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) + * + * ```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** (runtime type check) + * + * ```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) + * + * ```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** (runtime type check) + * + * ```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** (runtime type check) + * + * ```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) + * + * ```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 attaching or reading the stack-frame annotation consumed 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-smol/packages/effect/src/Channel.ts b/.repos/effect-smol/packages/effect/src/Channel.ts new file mode 100644 index 00000000000..dfefd78aaf9 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Channel.ts @@ -0,0 +1,8596 @@ +/** + * The `Channel` module provides the low-level stream processing primitive used + * to build Effect streams, sinks, and stream operators. + * + * A `Channel` describes a + * scoped process that can read elements from an upstream input, emit elements + * downstream, fail with a typed error, or complete with a typed done value. + * Most application code works with higher-level stream APIs; channels are for + * implementing reusable streaming primitives, adapting pull-based sources, and + * controlling how input, output, errors, final values, and resources compose. + * + * **Mental model** + * + * - `OutElem`, `OutErr`, and `OutDone` describe the values, failures, and final + * value produced by the channel. + * - `InElem`, `InErr`, and `InDone` describe the upstream protocol consumed by + * the channel when it is piped after another channel. + * - `Env` is the Effect environment required while the channel is interpreted. + * - Constructors such as {@link fromArray}, {@link fromIterable}, + * {@link fromEffect}, {@link succeed}, and {@link fail} create sources. + * - Combinators such as {@link map}, {@link mapEffect}, {@link flatMap}, and + * {@link pipeTo} transform, sequence, and connect channels. + * - Execution functions such as {@link runCollect} and {@link runDrain} + * interpret channels that no longer require upstream input. + * + * **Common tasks** + * + * - Build finite sources from values, arrays, iterables, queues, pub/sub + * subscriptions, effects, or pulls. + * - Transform output elements with pure or effectful functions. + * - Connect channels with {@link pipeTo} when one channel's output protocol + * should become another channel's input protocol. + * - Sequence dependent channels with {@link flatMap}, or concatenate channels + * with {@link concat}. + * - Manage channel-scoped resources with {@link acquireRelease} and + * {@link ensuring}. + * - Bridge to lower-level pull loops with {@link toPull} and {@link fromPull}. + * + * **Example** (Collecting transformed output) + * + * ```ts + * import { Channel, Effect } from "effect" + * + * const program = Channel.fromArray([1, 2, 3]).pipe( + * Channel.map((n) => n * 2), + * Channel.runCollect + * ) + * + * Effect.runPromise(program).then(console.log) + * ``` + * + * **Gotchas** + * + * - A channel's done value is distinct from its emitted elements; use + * done-focused APIs when the final value matters. + * - `pipeTo` connects the output side of the left channel to the input side of + * the right channel, so type errors usually mean those protocols do not line + * up. + * - Resource finalizers run when the channel scope closes, not when a channel + * value is merely constructed. + * - Prefer stream and sink APIs unless you are implementing lower-level + * streaming behavior. + * + * **See also** + * + * - {@link Channel} for the type parameters and variance of channel values. + * - {@link pipeTo} for wiring one channel into another. + * - {@link runCollect}, {@link runDrain}, and {@link runDone} for common + * execution modes. + * + * @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** (Using 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** (Using 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. + * + * **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`. + * + * **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. + * + * @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. + * + * **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** (FlatMapping 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 utils + * @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 utils + * @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 utils + * @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 utils + * @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 utils + * @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. + * + * **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. + * + * **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`. + * + * **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. + * + * **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. + * + * **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`, and the recovery needs 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. + * + * **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 utils + * @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. + * + * **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 utils + * @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 utils + * @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. + * + * **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 utils + * @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 utils + * @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 utils + * @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 utils + * @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 utils + * @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. + * + * **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. + * + * **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 utils + * @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 utils + * @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 utils + * @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 utils + * @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 utils + * @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. + * + * **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 utils + * @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 utils + * @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 utils + * @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 starting 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. + * + * **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-smol/packages/effect/src/ChannelSchema.ts b/.repos/effect-smol/packages/effect/src/ChannelSchema.ts new file mode 100644 index 00000000000..ce37de6733e --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/ChannelSchema.ts @@ -0,0 +1,344 @@ +/** + * The `ChannelSchema` module provides helpers for applying `Schema` encoding + * and decoding at `Channel` boundaries. It is useful when a channel should + * expose typed values to application code while communicating with an upstream + * or downstream component through an encoded representation such as JSON-ready + * data, wire protocol values, or any other schema-defined format. + * + * **Mental model** + * + * - A channel schema adapter is a streaming boundary: chunks flow through a + * `Channel`, and each non-empty chunk is validated and transformed with a + * `Schema` + * - `encode` turns typed schema values into their encoded representation before + * they leave a typed part of a pipeline + * - `decode` turns encoded input into typed schema values before application + * code consumes them + * - `duplex` wraps a bidirectional channel so callers work with typed input and + * output while the wrapped channel continues to operate on encoded chunks + * - Schema failures are surfaced through the channel error type as + * `SchemaError`, and schema services are reflected in the channel + * requirements + * + * **Common tasks** + * + * - Encode typed channel input before sending it to an encoded transport: + * {@link encode} + * - Decode encoded channel output before handling it as domain data: + * {@link decode} + * - Use unknown encoded boundaries when static encoded types are intentionally + * erased: {@link encodeUnknown} and {@link decodeUnknown} + * - Wrap a bidirectional encoded channel with typed input and output schemas: + * {@link duplex} or {@link duplexUnknown} + * + * **Gotchas** + * + * - These helpers operate on `NonEmptyReadonlyArray` chunks, so schemas are + * applied to non-empty batches rather than individual scalar values + * - Encoding and decoding can require services from the schema; those + * requirements become part of the resulting channel type + * - `duplex` encodes values flowing into the wrapped channel and decodes values + * emitted by it, so choose `inputSchema` and `outputSchema` from the + * perspective of the typed caller + * + * @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 the encoded input type is intentionally unknown or untyped, so + * that only the decoded output is 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 a bidirectional channel crosses an encoded boundary whose chunk types + * are intentionally erased, while callers should 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-smol/packages/effect/src/Chunk.ts b/.repos/effect-smol/packages/effect/src/Chunk.ts new file mode 100644 index 00000000000..d7d74117e71 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Chunk.ts @@ -0,0 +1,3002 @@ +/** + * The `Chunk` module provides an immutable, high-performance sequence data structure + * optimized for functional programming patterns. A `Chunk` is a persistent data structure + * that supports efficient append, prepend, and concatenation operations. + * + * ## What is a Chunk? + * + * A `Chunk` is an immutable sequence of elements of type `A` that provides: + * - **O(1) append and prepend operations** + * - **Efficient concatenation** through tree-like structure + * - **Memory efficiency** with structural sharing + * - **Rich API** with functional programming operations + * - **Type safety** with full TypeScript integration + * + * ## Key Features + * + * - **Immutable**: All operations return new chunks without modifying the original + * - **Efficient**: Optimized data structure with logarithmic complexity for most operations + * - **Functional**: Rich set of transformation and combination operators + * - **Lazy evaluation**: Many operations are deferred until needed + * - **Interoperable**: Easy conversion to/from arrays and other collections + * + * ## Performance Characteristics + * + * - **Append/Prepend**: O(1) amortized + * - **Random Access**: O(log n) + * - **Concatenation**: O(log min(m, n)) + * - **Iteration**: O(n) + * - **Memory**: Structural sharing minimizes allocation + * + * **Example** (Creating and combining chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * // Creating chunks + * const chunk1 = Chunk.fromIterable([1, 2, 3]) + * const chunk2 = Chunk.fromIterable([4, 5, 6]) + * const empty = Chunk.empty() + * + * // Combining chunks + * const combined = Chunk.appendAll(chunk1, chunk2) + * console.log(Chunk.toReadonlyArray(combined)) // [1, 2, 3, 4, 5, 6] + * ``` + * + * **Example** (Transforming chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * // Functional transformations + * const numbers = Chunk.range(1, 5) // [1, 2, 3, 4, 5] + * const doubled = Chunk.map(numbers, (n) => n * 2) // [2, 4, 6, 8, 10] + * const evens = Chunk.filter(doubled, (n) => n % 4 === 0) // [4, 8] + * const sum = Chunk.reduce(evens, 0, (acc, n) => acc + n) // 12 + * ``` + * + * **Example** (Processing chunks with Effect) + * + * ```ts + * import { Chunk, Effect } from "effect" + * + * // Working with Effects + * const processChunk = Effect.fnUntraced(function*(chunk: Chunk.Chunk) { + * const mapped = Chunk.map(chunk, (n) => n * 2) + * const filtered = Chunk.filter(mapped, (n) => n > 5) + * return Chunk.toReadonlyArray(filtered) + * }) + * ``` + * + * @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 equivalence + * @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, so mutating the source array can + * mutate the 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, so mutating + * the source array can mutate the 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 unsafely, will throw on 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 elements and get a non-empty + * result. + * + * **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 concatenating + * @since 2.0.0 + */ +export const append: { + (a: A2): (self: Chunk) => NonEmptyChunk + (self: Chunk, a: A2): NonEmptyChunk +} = dual(2, (self: Chunk, a: A2): NonEmptyChunk => appendAll(self, of(a))) + +/** + * 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 concatenating + * @since 2.0.0 + */ +export const prepend: { + (elem: B): (self: Chunk) => NonEmptyChunk + (self: Chunk, elem: B): NonEmptyChunk +} = dual(2, (self: Chunk, elem: B): NonEmptyChunk => appendAll(of(elem), self)) + +/** + * Takes the first up to `n` elements from the chunk. + * + * **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 concatenating + * @since 2.0.0 + */ +export const prependAll: { + , T extends Chunk>( + that: T + ): (self: S) => Chunk.OrNonEmpty | Chunk.Infer> + (self: Chunk, that: NonEmptyChunk): NonEmptyChunk + (self: NonEmptyChunk, that: Chunk): NonEmptyChunk + (self: Chunk, that: Chunk): Chunk +} = dual(2, (self: NonEmptyChunk, that: Chunk): Chunk => appendAll(that, self)) + +/** + * Concatenates two chunks, combining their elements. + * If either chunk is non-empty, the result is also a non-empty chunk. + * + * **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 concatenating + * @since 2.0.0 + */ +export const appendAll: { + , T extends Chunk>( + that: T + ): (self: S) => Chunk.OrNonEmpty | Chunk.Infer> + (self: Chunk, that: NonEmptyChunk): NonEmptyChunk + (self: NonEmptyChunk, that: Chunk): NonEmptyChunk + (self: Chunk, that: Chunk): Chunk +} = dual(2, (self: Chunk, that: Chunk): Chunk => { + if (self.backing._tag === "IEmpty") { + return that + } + if (that.backing._tag === "IEmpty") { + return self + } + const diff = that.depth - self.depth + if (Math.abs(diff) <= 1) { + return makeChunk({ _tag: "IConcat", left: self, right: that }) + } else if (diff < -1) { + if (self.left.depth >= self.right.depth) { + const nr = appendAll(self.right, that) + return makeChunk({ _tag: "IConcat", left: self.left, right: nr }) + } else { + const nrr = appendAll(self.right.right, that) + if (nrr.depth === self.depth - 3) { + const nr = makeChunk({ _tag: "IConcat", left: self.right.left, right: nrr }) + return makeChunk({ _tag: "IConcat", left: self.left, right: nr }) + } else { + const nl = makeChunk({ _tag: "IConcat", left: self.left, right: self.right.left }) + return makeChunk({ _tag: "IConcat", left: nl, right: nrr }) + } + } + } else { + if (that.right.depth >= that.left.depth) { + const nl = appendAll(self, that.left) + return makeChunk({ _tag: "IConcat", left: nl, right: that.right }) + } else { + const nll = appendAll(self, that.left.left) + if (nll.depth === that.depth - 3) { + const nl = makeChunk({ _tag: "IConcat", left: nll, right: that.left.right }) + return makeChunk({ _tag: "IConcat", left: nl, right: that.right }) + } else { + const nr = makeChunk({ _tag: "IConcat", left: that.left.right, right: that.right }) + return makeChunk({ _tag: "IConcat", left: nll, right: nr }) + } + } + } +}) + +/** + * Returns a filtered and mapped subset of the elements. + * + * **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 batches 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. + * + * **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. + * + * **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-smol/packages/effect/src/Clock.ts b/.repos/effect-smol/packages/effect/src/Clock.ts new file mode 100644 index 00000000000..5916c2a4f73 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Clock.ts @@ -0,0 +1,246 @@ +/** + * The `Clock` module provides functionality for time-based operations in Effect applications. + * It offers precise time measurements, scheduling capabilities, and controlled time management + * for testing scenarios. + * + * The Clock service is a core component of the Effect runtime, providing: + * - Current time access in milliseconds and nanoseconds + * - Sleep operations for delaying execution + * - Time-based scheduling primitives + * - Testable time control through `TestClock` + * + * ## Key Features + * + * - **Precise timing**: Access to both millisecond and nanosecond precision + * - **Sleep operations**: Non-blocking sleep with proper interruption handling + * - **Service integration**: Seamless integration with Effect's dependency injection + * - **Testable**: Mock time control for deterministic testing + * - **Resource-safe**: Automatic cleanup of time-based resources + * + * **Example** (Measuring elapsed time) + * + * ```ts + * import { Clock, Effect } from "effect" + * + * // Get current time in milliseconds + * const getCurrentTime = Clock.currentTimeMillis + * + * // Sleep for 1 second + * const sleep1Second = Effect.sleep("1 seconds") + * + * // Measure execution time + * const measureTime = Effect.gen(function*() { + * const start = yield* Clock.currentTimeMillis + * yield* Effect.sleep("100 millis") + * const end = yield* Clock.currentTimeMillis + * return end - start + * }) + * ``` + * + * **Example** (Using the Clock service) + * + * ```ts + * import { Clock, Effect } from "effect" + * + * // Using Clock service directly + * const program = Effect.gen(function*() { + * const clock = yield* Clock.Clock + * const currentTime = yield* clock.currentTimeMillis + * console.log(`Current time: ${currentTime}`) + * + * // Sleep for 500ms + * yield* Effect.sleep("500 millis") + * + * const afterSleep = yield* clock.currentTimeMillis + * console.log(`After sleep: ${afterSleep}`) + * }) + * ``` + * + * @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 current Clock service in the environment. + * + * **When to use** + * + * Use when you need to access or provide the full Clock service 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** (Using 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-smol/packages/effect/src/Combiner.ts b/.repos/effect-smol/packages/effect/src/Combiner.ts new file mode 100644 index 00000000000..c4349c9bb77 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Combiner.ts @@ -0,0 +1,371 @@ +/** + * A module for combining two values of the same type into one. + * + * A `Combiner` wraps a single binary function `(self: A, that: A) => A`. + * It describes *how* two values merge but carries no initial/empty value + * (for that, see `Reducer` which extends `Combiner` with an + * `initialValue`). + * + * ## Mental model + * + * - **Combiner** – an object with a `combine(self, that)` method that returns + * a value of the same type. + * - **Argument order** – `self` is the "left" / accumulator side, `that` is + * the "right" / incoming side. + * - **No identity element** – unlike a monoid, a `Combiner` does not require + * a neutral element. Use `Reducer` when you need one. + * - **Purity** – all combiners produced by this module are pure; they never + * mutate their arguments. + * - **Composability** – combiners can be lifted into `Option`, `Struct`, + * `Tuple`, and other container types via helpers in those modules. + * + * ## Common tasks + * + * - Create a combiner from any binary function → {@link make} + * - Swap argument order → {@link flip} + * - Pick the smaller / larger of two values → {@link min} / {@link max} + * - Always keep the first or last value → {@link first} / {@link last} + * - Ignore both values and return a fixed result → {@link constant} + * - Insert a separator between combined values → {@link intercalate} + * + * ## Gotchas + * + * - `min` and `max` require an `Order`, not a raw comparator. Import from + * e.g. `Number.Order` or `String.Order`. + * - `intercalate` is curried: call it with the separator first, then pass the + * base combiner. + * - A `Reducer` (which adds `initialValue`) is also a valid `Combiner` — you + * can pass a `Reducer` anywhere a `Combiner` is expected. + * + * ## Quickstart + * + * **Example** (combining strings with a separator) + * + * ```ts + * import { Combiner, String } from "effect" + * + * const csv = Combiner.intercalate(",")(String.ReducerConcat) + * + * console.log(csv.combine("a", "b")) + * // Output: "a,b" + * + * console.log(csv.combine(csv.combine("a", "b"), "c")) + * // Output: "a,b,c" + * ``` + * + * ## See also + * + * - {@link make} – the primary constructor + * - {@link Combiner} – the core interface + * + * @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** (number addition combiner) + * + * ```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 the "right" value should act as the accumulator side, or when + * you want to reverse the natural direction of 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** (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** (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, or + * when the combining logic should keep the existing value. + * + * **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, or + * when each new value should replace the accumulator. + * + * **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 a combiner should produce a fixed result regardless of input, + * or when a generic API needs a combiner but the combined value 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 are building delimited strings (CSV, paths, etc.) by + * repeated combination, or when you need to inject a fixed separator between + * accumulated values. + * + * **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-smol/packages/effect/src/Config.ts b/.repos/effect-smol/packages/effect/src/Config.ts new file mode 100644 index 00000000000..064d2edba53 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Config.ts @@ -0,0 +1,1474 @@ +/** + * Declarative, schema-driven configuration loading. A `Config` describes + * how to read and validate a value of type `T` from a `ConfigProvider`. Configs + * can be composed, transformed, and used directly as Effects. + * + * ## Mental model + * + * - **Config\** – a recipe for extracting a typed value from a + * `ConfigProvider`. Created via convenience constructors or {@link schema}. + * - **ConfigProvider** – the backing data source (env vars, JSON, `.env` + * files). See the `ConfigProvider` module. + * - **ConfigError** – wraps either a `SourceError` (provider I/O failure) or + * a `SchemaError` (validation / decoding failure). + * - **parse** – instance method on every `Config` that takes a provider and + * returns `Effect`. + * - **Yieldable** – every `Config` can be yielded inside `Effect.gen`. It + * automatically resolves the current `ConfigProvider` from the context. + * + * ## Common tasks + * + * - Read a single env var → {@link string}, {@link number}, {@link boolean}, + * {@link int}, {@link port}, {@link url}, {@link date}, {@link duration}, + * {@link logLevel}, {@link redacted} + * - Read a structured config → {@link schema} with a `Schema.Struct` + * - Provide a default → {@link withDefault} + * - Make a config optional → {@link option} + * - Transform a value → {@link map} / {@link mapOrFail} + * - Fall back on error → {@link orElse} + * - Combine multiple configs → {@link all} + * - Build from a `Schema.Codec` → {@link schema} + * - Always succeed or fail → {@link succeed} / {@link fail} + * + * ## Gotchas + * + * - `withDefault` and `option` only apply when the error is caused by + * **missing data**. Validation errors (wrong type, out of range) still + * propagate. + * - When yielded in `Effect.gen`, the config resolves using the current + * `ConfigProvider` service. To use a specific provider, call `.parse(provider)` + * instead. + * - The `name` parameter on convenience constructors (e.g. `Config.string("HOST")`) + * sets the root path segment. Omit it when the config is part of a larger + * schema. + * + * ## Quickstart + * + * **Example** (Reading typed config from environment variables) + * + * ```ts + * import { Config, ConfigProvider, Effect, Schema } from "effect" + * + * const AppConfig = Config.schema( + * Schema.Struct({ + * host: Schema.String, + * port: Schema.Int + * }), + * "app" + * ) + * + * const provider = ConfigProvider.fromEnv({ + * env: { app_host: "localhost", app_port: "8080" } + * }) + * + * // Effect.runSync(AppConfig.parse(provider)) + * // { host: "localhost", port: 8080 } + * ``` + * + * @see {@link schema} – build a Config from any Schema.Codec + * @see {@link ConfigError} – the error type for config failures + * @see {@link make} – low-level Config constructor + * + * @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 AST from "./SchemaAST.ts" +import * as Issue from "./SchemaIssue.ts" +import * as Parser from "./SchemaParser.ts" +import * as Transformation from "./SchemaTransformation.ts" + +const TypeId = "~effect/Config" + +/** + * Returns `true` if `u` is a `Config` instance. + * + * **When to use** + * + * Use when runtime type-checking before calling `.parse()` on an unknown value. + * - Distinguishing a `Config` from a plain value inside {@link unwrap}. + * + * **Example** (Type guard) + * + * ```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 match on `error.cause._tag` to distinguish source failures from + * validation failures. + * - Pass to {@link fail} to create a Config that always errors. + * + * **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)` – runs the config against a specific provider, + * returning `Effect`. + * - 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 + * @see {@link make} – low-level constructor + * + * @category models + * @since 2.0.0 + */ +export interface Config extends Effect.Effect { + readonly [TypeId]: typeof TypeId + readonly parse: (provider: ConfigProvider.ConfigProvider) => 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" + } + } +} + +/** + * Creates a `Config` from a raw parsing function. + * + * **When to use** + * + * Use to build a custom config that cannot be expressed with {@link schema} or + * convenience constructors, or to compose configs programmatically. + * + * **Details** + * + * The `parse` callback receives a `ConfigProvider` and must return + * `Effect`. + * + * **Example** (Custom config that reads two keys) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const hostPort = Config.make((provider) => + * Effect.all({ + * host: Config.string("host").parse(provider), + * port: Config.number("port").parse(provider) + * }) + * ) + * + * const provider = ConfigProvider.fromUnknown({ host: "localhost", port: 3000 }) + * // Effect.runSync(hostPort.parse(provider)) + * // { host: "localhost", port: 3000 } + * ``` + * + * @see {@link schema} – higher-level constructor using Schema codecs + * + * @category constructors + * @since 4.0.0 + */ +export function make( + parse: (provider: ConfigProvider.ConfigProvider) => Effect.Effect +): Config { + const self = Object.create(Proto) + self.parse = parse + return self +} + +/** + * Transforms the parsed value of a config with a pure function. + * + * **When to use** + * + * Use when post-processing a config value (e.g. trimming, uppercasing, wrapping). + * - The transformation cannot fail. Use {@link mapOrFail} if it can. + * + * **Details** + * + * Supports both data-last and data-first calling conventions. + * + * **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) => Effect.map(self.parse(provider), f)) +}) + +/** + * Transforms the parsed value with a function that may fail. + * + * **When to use** + * + * Use to validate or converting a config value where the transformation can + * produce a `ConfigError` (e.g. parsing a URL, checking a range). + * + * **Details** + * + * Supports both data-last and data-first calling conventions. + * + * **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) => Effect.flatMap(self.parse(provider), f)) +}) + +/** + * Provides a fallback config when parsing fails with a `ConfigError`. + * + * **When to use** + * + * Use when trying an alternative config source when the primary one errors. + * - Providing environment-specific overrides. + * + * **Details** + * + * Unlike {@link withDefault}, this catches **all** `ConfigError`s (not just + * missing data). The fallback function receives the error and returns a new + * `Config`. + * + * Supports both data-last and data-first calling conventions. + * + * **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) => Effect.catch(self.parse(provider), (error) => that(error).parse(provider))) +}) + +/** + * Combines multiple configs into a single config that parses all of them. + * + * **When to use** + * + * Use when grouping 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) => Effect.all(configs.map((config) => config.parse(provider)))) as any + } else { + return make((provider) => Effect.all(Rec.map(configs, (config) => config.parse(provider)))) as any + } +} + +function isMissingDataOnly(issue: Issue.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": + case "Filter": + return isMissingDataOnly(issue.issue) + case "UnexpectedKey": + return false + case "Forbidden": + return false + case "Composite": + case "AnyOf": + return issue.issues.every(isMissingDataOnly) + } +} + +/** + * Provides a fallback value when the config fails due to missing data. + * + * **When to use** + * + * Use when making a config key optional with a sensible default. + * + * **Details** + * + * The default is lazily evaluated. Supports both data-last and data-first + * calling conventions. + * + * **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 a config key may or may not be present and you want to handle both + * cases explicitly. + * + * **Gotchas** + * + * Like {@link withDefault}, only missing-data errors produce `None`. + * Validation errors still propagate. + * + * **Example** (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) => { + const entries = Object.entries(wrapped) + const configs = entries.map(([key, config]) => + unwrap(config as any).parse(provider).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: AST.AST, + provider: ConfigProvider.ConfigProvider, + path: Path +) => Effect.Effect = Effect.fnUntraced( + function*(ast, provider, path) { + switch (ast._tag) { + case "Objects": { + 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) { + const stat = yield* provider.load(path) + if (stat && stat._tag === "Record") { + for (const is of ast.indexSignatures) { + const matches = Parser._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 && stat._tag === "Value") return stat.value + const out: Array = [] + for (let i = 0; i < ast.elements.length; i++) { + out.push(yield* recur(ast.elements[i], 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 reading structured or validated config (structs, arrays, unions, branded + * types, etc.). + * - All convenience constructors (`string`, `number`, …) delegate to this. + * + * **Details** + * + * The optional `path` sets the root path segment(s) for the config lookup. + * Pass a single string for a flat key or an array for nested paths. + * + * 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.Codec, path?: string | ConfigProvider.Path): Config { + const codecStringTree = Schema.toCodecStringTree(codec) + const decodeUnknownEffect = Parser.decodeUnknownEffect(codecStringTree) + const codecStringTreeEncoded = AST.toEncoded(codecStringTree.ast) + const defaultPath = typeof path === "string" ? [path] : path ?? [] + return make((provider) => { + const path = provider.prefix ? [...provider.prefix, ...defaultPath] : defaultPath + return recur(codecStringTreeEncoded, provider, defaultPath).pipe( + Effect.flatMapEager((tree) => + decodeUnknownEffect(tree).pipe( + Effect.mapErrorEager((issue) => + new Schema.SchemaError(path.length > 0 ? new Issue.Pointer(path, 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 passing to {@link schema} for custom paths, or use the + * {@link boolean} convenience constructor. + * + * **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, + Transformation.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 passing to {@link schema} for custom paths, or use the {@link port} + * convenience constructor. + * + * @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 passing to {@link schema} for custom paths, or use the + * {@link logLevel} convenience constructor. + * + * **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), + Transformation.splitKeyValue(options) + ), + Schema.decodeTo(record) + ) + + return Schema.Union([record, recordString]) +} + +// ----------------------------------------------------------------------------- +// constructors +// ----------------------------------------------------------------------------- + +/** + * Creates a config that always fails with the given error. + * + * **When to use** + * + * Use when inside {@link orElse} to re-raise a specific error. + * - Testing error handling paths. + * + * @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 providing a hardcoded constant inside {@link orElse}. + * - Testing. + * + * **Example** (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 to read a numeric config value when `NaN` and `Infinity` are acceptable. + * + * **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 grouping related config keys under a common namespace (e.g. + * `"database"`, `"redis"`). + * - Building reusable config fragments that callers nest at different paths. + * + * **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** (Env vars with 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) => self.parse(ConfigProvider.nested(provider, name))) +) diff --git a/.repos/effect-smol/packages/effect/src/ConfigProvider.ts b/.repos/effect-smol/packages/effect/src/ConfigProvider.ts new file mode 100644 index 00000000000..81ef100924a --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/ConfigProvider.ts @@ -0,0 +1,1207 @@ +/** + * Provides the data source layer for the `Config` module. A `ConfigProvider` + * knows how to load raw configuration nodes from a backing store (environment + * variables, JSON objects, `.env` files, file trees) and expose them through a + * uniform `Node` interface that `Config` schemas consume. + * + * ## Mental model + * + * - **Node** – a discriminated union (`Value | Record | Array`) that describes + * what lives at a given path in the configuration tree. + * - **Path** – an array of string or numeric segments used to address a node + * (e.g. `["database", "host"]`). + * - **ConfigProvider** – an object with a `load(path)` method that resolves a + * path to a `Node | undefined`. Providers can be composed and transformed. + * - **Context.Reference** – `ConfigProvider` is registered as a reference + * service that defaults to `fromEnv()`, so it works without explicit + * provision. + * - **SourceError** – the typed error returned when a backing store is + * unreadable (I/O failure, permission error, etc.). + * + * ## Common tasks + * + * - Read from environment variables → {@link fromEnv} + * - Read from a JSON / plain object → {@link fromUnknown} + * - Parse a `.env` string → {@link fromDotEnvContents} + * - Load a `.env` file → {@link fromDotEnv} + * - Read from a directory tree → {@link fromDir} + * - Build a custom provider → {@link make} + * - Fall back to another provider → {@link orElse} + * - Scope a provider under a prefix → {@link nested} + * - Convert path segments to `CONSTANT_CASE` → {@link constantCase} + * - Transform path segments arbitrarily → {@link mapInput} + * - Install a provider as a Layer → {@link layer} / {@link layerAdd} + * + * ## Gotchas + * + * - `fromEnv` joins path segments with `_` for lookup **and** splits env var + * names on `_` to discover child keys. `DATABASE_HOST=x` is therefore + * accessible at both `["DATABASE_HOST"]` and `["DATABASE", "HOST"]`. + * - Because of `_` splitting, querying a parent path like `["DATABASE"]` + * returns a `Record` node with child key `"HOST"`, even if no env var + * named `DATABASE` exists. + * - When using `fromEnv` with schemas that use camelCase keys, pipe the + * provider through {@link constantCase} so `databaseHost` resolves to + * `DATABASE_HOST`. + * - `orElse` only falls back when the primary provider returns `undefined` + * (path not found). It does **not** catch `SourceError`. + * - `nested` prepends segments to the path *after* `mapInput` has run, so + * the order of composition matters. + * + * ## Quickstart + * + * **Example** (Reading config from environment variables) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const provider = ConfigProvider.fromEnv({ + * env: { APP_PORT: "3000", APP_HOST: "localhost" } + * }) + * + * const port = Config.number("port") + * + * const program = port.parse( + * provider.pipe( + * ConfigProvider.nested("app"), + * ConfigProvider.constantCase + * ) + * ) + * + * // Effect.runSync(program) // 3000 + * ``` + * + * @see {@link make} – build a provider from a lookup function + * @see {@link fromEnv} – the default provider backed by `process.env` + * @see {@link fromUnknown} – provider backed by a plain JS object + * + * @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 describing 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 describing a JSON array or a set of 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 use this from a custom provider's `get` callback when the underlying store + * is unreachable or produces an I/O error, or match on it in error channels + * when consuming provider output directly. + * + * **Gotchas** + * + * Do not use `SourceError` for "key not found". That case is represented by + * returning `undefined` from `load` or `get`. + * + * **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`/`get` 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)` resolves `mapInput` and `prefix` transformations, then + * delegates to `get`. This is what the `Config` module calls. `get(path)` is + * raw access to the underlying store without path transformations. + * `mapInput` and `prefix` are optional path transformations set by + * {@link mapInput} and {@link nested}. All methods return + * `Effect`: `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 + + /** + * Raw access to the underlying source. + * + * **When to use** + * + * Use to read from the backing source without applying this provider's path + * transformations. + */ + readonly get: (path: Path) => Effect.Effect + + /** + * Function to map the input path. + * + * **When to use** + * + * Use to store the path transformation applied before raw provider lookup. + */ + readonly mapInput: ((path: Path) => Path) | undefined + + /** + * Prefix to add to the input path. + * + * **When to use** + * + * Use to store the path prefix applied before raw provider lookup. + */ + readonly prefix: Path | undefined +} + +/** + * 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 provider for an entire program via + * `Effect.provideService(ConfigProvider.ConfigProvider, myProvider)`, or to + * retrieve the current provider inside an Effect with + * `yield* ConfigProvider.ConfigProvider`. + * + * **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" + } + } +} + +/** + * 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. + * + * The optional `mapInput` and `prefix` parameters are wired into the + * resulting `load` method so that combinators like {@link mapInput} and + * {@link nested} can compose without wrapping `get`. + * + * **Example** (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, + mapInput?: (path: Path) => Path, + prefix?: Path +): ConfigProvider { + const self = Object.create(Proto) + self.get = get + self.mapInput = mapInput + self.prefix = prefix + self.load = (path: Path) => { + if (mapInput) path = mapInput(path) + if (prefix) path = [...prefix, ...path] + return get(path) + } + return self +} + +/** + * 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 to provide partial overrides on top of a base config. + * + * **Details** + * + * Supports both data-last and data-first calling conventions. + * + * **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 => + make((path) => Effect.flatMap(self.get(path), (node) => node ? Effect.succeed(node) : that.get(path))) +) + +/** + * Transforms the path segments before they reach the underlying store. + * + * **When to use** + * + * Use when you use this for renaming or re-casing path segments, or for adding suffixes and + * other per-segment transformations. See {@link constantCase} for a common + * specialization. + * + * **Details** + * + * The function `f` receives the full path and must return a new path. If the + * provider already has a `mapInput`, the functions compose: the existing + * mapping runs first, then `f`. Supports both data-last and data-first calling + * conventions. + * + * **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 => { + return make(self.get, self.mapInput ? flow(self.mapInput, f) : f, self.prefix ? f(self.prefix) : undefined) + } +) + +/** + * 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. 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.constantCase(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 + * to reuse the same provider shape for multiple sub-configs. + * + * **Details** + * + * Accepts a single string or a full `Path` array. Supports both data-last and + * data-first calling conventions. + * + * **Gotchas** + * + * The prefix is prepended after any `mapInput` transformation runs, so + * ordering matters when composing with {@link mapInput} or + * {@link constantCase}. + * + * **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 + return make(self.get, self.mapInput, self.prefix ? [...self.prefix, ...path] : 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** (Using a JSON object as the config source) + * + * ```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 to 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 use this in unit or integration tests where you want deterministic config + * without touching the environment, or when embedding config directly in code + * or reading a JSON file. + * + * **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(...)`. + * + * **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): ConfigProvider { + return make((path) => Effect.succeed(nodeAtJson(root, path))) +} + +function nodeAtJson(root: unknown, path: Path): 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) +} + +function describeUnknown(u: unknown): Node | undefined { + if (u === undefined || u === null) return undefined + if (typeof u === "string") return makeValue(u) + 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)) +} + +/** + * 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 to 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: { ... } }`. + * + * 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 }): ConfigProvider { + const env = options?.env ?? { + ...globalThis?.process?.env, + ...(import.meta as any)?.env + } + + const trie = buildEnvTrie(env) + + return make((path) => Effect.succeed(nodeAtEnv(trie, env, path))) +} + +type EnvTrieNode = { + value?: string + children?: Record +} + +function buildEnvTrie(env: Record): EnvTrieNode { + const root: 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 = root + for (const seg of segments) { + node.children ??= {} + node = node.children[seg] ??= {} + } + + // co-located value at this node + node.value = value + } + + return root +} + +const NUMERIC_INDEX = /^(0|[1-9][0-9]*)$/ + +function nodeAtEnv(trie: EnvTrieNode, env: Record, path: Path): Node | undefined { + const key = path.map(String).join("_") + const leafValue = env[key] + + 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. Use + * {@link fromDotEnv} instead if you want to read a `.env` file from disk. + * + * **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 }`. + * + * 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 +}): ConfigProvider { + let env = parseDotEnvContents(lines) + if (options?.expandVariables) { + env = dotEnvExpand(env) + } + return fromEnv({ env }) +} + +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. Use {@link fromDotEnvContents} if you already have the file + * contents as a string. + * + * **Details** + * + * Requires `FileSystem` in the Effect context. Defaults to reading `".env"` in + * the current directory; override with `{ path: "/custom/.env" }`. + * + * 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 constructors + * @since 4.0.0 + */ +export const fromDotEnv: (options?: { + readonly path?: string | undefined + readonly expandVariables?: boolean | undefined +}) => Effect.Effect = Effect.fnUntraced( + function*(options) { + const fs = yield* FileSystem.FileSystem + const content = yield* fs.readFileString(options?.path ?? ".env") + return fromEnv({ env: parseDotEnvContents(content) }) + } +) + +/** + * 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 use this for Kubernetes ConfigMap or Secret volume mounts, where each key is + * a file under a mount path, or for any file-per-key configuration layout. + * + * **Details** + * + * Resolution tries a regular file first and returns a `Value` node with + * 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, it + * returns `SourceError`. + * + * Requires `Path` and `FileSystem` in the Effect context. Defaults to root + * path `/`; override with `{ rootPath: "/etc/config" }`. + * + * **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 +}) => 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 ?? "/" + + 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) => makeValue(content.trim())) + ) + + // If not a file, try reading as a *directory* + const asDirectory = fs.readDirectory(fullPath).pipe( + Effect.map((entries: ReadonlyArray) => { + // Support both string paths and DirEntry-like objects + const keys = entries.map((e) => typeof e === "string" ? platformPath.basename(e) : format(e?.name ?? "")) + return makeRecord(new Set(keys)) + }) + ) + + return asFile.pipe( + Effect.catch(() => asDirectory), + Effect.mapError((cause: PlatformError) => + new SourceError({ + message: `Failed to read file at ${platformPath.join(rootPath, ...path.map(String))}`, + cause + }) + ) + ) + }) +}) diff --git a/.repos/effect-smol/packages/effect/src/Console.ts b/.repos/effect-smol/packages/effect/src/Console.ts new file mode 100644 index 00000000000..aa44e6c7576 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Console.ts @@ -0,0 +1,717 @@ +/** + * The `Console` module provides a functional interface for console operations within + * the Effect ecosystem. It offers type-safe logging, debugging, and console manipulation + * capabilities with built-in support for testing and environment isolation. + * + * ## Key Features + * + * - **Type-safe logging**: All console operations return Effects for composability + * - **Testable**: Mock console output for testing scenarios + * - **Service-based**: Integrated with Effect's dependency injection system + * - **Environment isolation**: Different console implementations per environment + * - **Rich API**: Support for all standard console methods (log, error, debug, etc.) + * - **Performance tracking**: Built-in timing and profiling capabilities + * + * ## Core Operations + * + * - **Basic logging**: `log`, `error`, `warn`, `info`, `debug` + * - **Assertions**: `assert` for conditional logging + * - **Grouping**: `group`, `groupCollapsed`, `groupEnd` for organized output + * - **Timing**: `time`, `timeEnd`, `timeLog` for performance measurement + * - **Data display**: `table`, `dir`, `dirxml` for structured data visualization + * - **Utilities**: `clear`, `count`, `countReset`, `trace` + * + * **Example** (Logging basic messages) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // Basic logging + * const program = Effect.gen(function*() { + * yield* Console.log("Hello, World!") + * yield* Console.error("Something went wrong") + * yield* Console.warn("This is a warning") + * yield* Console.info("Information message") + * }) + * ``` + * + * **Example** (Grouping timed logs) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // Grouped logging with timing + * const debugProgram = Console.withGroup( + * Effect.gen(function*() { + * yield* Console.log("Step 1: Loading...") + * yield* Effect.sleep("100 millis") + * + * yield* Console.log("Step 2: Processing...") + * yield* Effect.sleep("200 millis") + * }), + * { label: "Processing Data" } + * ) + * ``` + * + * **Example** (Displaying structured data) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // Data visualization and debugging + * const dataProgram = Effect.gen(function*() { + * const users = [ + * { id: 1, name: "Alice", age: 30 }, + * { id: 2, name: "Bob", age: 25 } + * ] + * + * yield* Console.table(users) + * yield* Console.dir(users[0], { depth: 2 }) + * yield* Console.assert(users.length > 0, "Users array should not be empty") + * }) + * ``` + * + * @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 an Effect program needs the current console service as a context + * reference, such as when providing or overriding a console implementation. + * + * **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** (Using 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-smol/packages/effect/src/Context.ts b/.repos/effect-smol/packages/effect/src/Context.ts new file mode 100644 index 00000000000..85fb22d5a88 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Context.ts @@ -0,0 +1,1382 @@ +/** + * The `Context` module implements Effect's typed service environment. A + * `Context` is an immutable collection of service implementations, + * keyed by `Context.Service` or `Context.Reference` values. The type parameter + * records which service identifiers are present so effects can require + * dependencies without passing every implementation as an argument. + * + * **Mental model** + * + * - A service key is both a runtime identifier and a typed handle for one + * dependency + * - `Context.Service` creates keys for dependencies that must be provided by + * the surrounding context + * - `Context.Reference` creates keys that have a cached default value when no + * explicit implementation is stored + * - A `Context` value stores implementations, while Effect fibers carry the + * active context used to satisfy service requirements + * - Context operations such as {@link add}, {@link merge}, {@link pick}, and + * {@link omit} return new contexts instead of mutating the original one + * + * **Common tasks** + * + * - Create service keys: {@link Service}, {@link Reference} + * - Build contexts: {@link empty}, {@link make}, {@link add}, {@link merge} + * - Read services: {@link get}, {@link getOption}, {@link getOrElse} + * - Keep or remove selected services: {@link pick}, {@link omit} + * + * **Example** (Building and reading a context) + * + * ```ts + * import { Context } from "effect" + * + * const Logger = Context.Service<{ log: (message: string) => void }>("Logger") + * + * const context = Context.make(Logger, { + * log: (message) => console.log(message) + * }) + * + * const logger = Context.get(context, Logger) + * logger.log("started") + * ``` + * + * **Gotchas** + * + * - The service key string is the runtime identity; reusing the same string for + * unrelated services makes them occupy the same slot + * - `Context.Reference` defaults are used only when no explicit value is stored + * for that key + * + * @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 type { ErrorWithStackTraceLimit } from "./internal/tracer.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 a dependency must be provided by the surrounding context. Use + * `Reference` when a dependency should have a default value. + * + * **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 = (Error as ErrorWithStackTraceLimit).stackTraceLimit + ;(Error as ErrorWithStackTraceLimit) + .stackTraceLimit = 2 + const err = new Error() + ;(Error as ErrorWithStackTraceLimit).stackTraceLimit = 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 without validating or + * copying it. + * + * **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 always have a service value to store. Use `addOrOmit` + * when the value is optional and a missing value should remove the service. + * + * **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 service presence is already represented as an `Option`. + * Use `add` when you always want to store a service value. + * + * **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 want a fallback value for a missing regular + * service. Use `getOption` when you need to distinguish presence from absence. + * + * **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 raw map-style lookup. Use `getOption` when you want the + * usual `Context.Reference` default-value behavior. + * + * **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 the context type cannot prove that the service is present. Use + * `get` when the service requirement is tracked in the context type, or + * `getOption` when absence is expected. + * + * **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 the context type proves that the service is present. Use + * `getOption` or `getOrElse` when a service may be absent. + * + * **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 to resolve a `Context.Reference` against a context when you want either + * the 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 service absence is expected and should be represented + * as data. Use `getOrElse` when you want to provide a fallback value directly. + * + * **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 combining two contexts. Use `mergeAll` when combining a + * variadic list of 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 the number of contexts is variadic. Use `merge` when + * combining exactly two 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 a small allowlist of services. Use `omit` + * when it is easier to name the services to remove. + * + * **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 small denylist of services. Use `pick` + * when it is easier to name the services to keep. + * + * **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 utils + * @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 a service should be available even if it is not + * explicitly stored in the `Context`. Use `Service` when the service must be + * provided by the surrounding context. + * + * **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-smol/packages/effect/src/Cron.ts b/.repos/effect-smol/packages/effect/src/Cron.ts new file mode 100644 index 00000000000..d07f9a10489 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Cron.ts @@ -0,0 +1,1248 @@ +/** + * The `Cron` module provides utilities for representing recurring calendar + * schedules with cron expressions. A `Cron` value stores allowed seconds, + * minutes, hours, days of month, months, weekdays, and an optional time zone, + * then uses those constraints to test dates and find scheduled occurrences. + * + * **Mental model** + * + * - A cron schedule is a set of allowed values for each time field + * - Expressions may use five fields (`minute hour day month weekday`) or six + * fields (`second minute hour day month weekday`); five-field expressions + * default seconds to `0` + * - Each field supports `*`, comma-separated values, ranges, and step syntax + * - Month and weekday fields support aliases such as `JAN`, `DEC`, `SUN`, and + * `MON` + * - Empty internal field sets represent an unconstrained field, the same idea + * as `*` + * - When both day-of-month and weekday are constrained, matching uses cron's + * inclusive behavior: either field may match + * + * **Common tasks** + * + * - Build directly from field constraints: {@link make} + * - Parse expressions safely: {@link parse} + * - Parse expressions and throw on invalid input: {@link parseUnsafe} + * - Check whether a date satisfies a schedule: {@link match} + * - Find adjacent scheduled dates: {@link next}, {@link prev} + * - Iterate future scheduled dates: {@link sequence} + * - Compare schedule constraints: {@link equals}, {@link Equivalence} + * - Detect parse failures: {@link CronParseError}, {@link isCronParseError} + * + * **Gotchas** + * + * - Weekdays are numbered `0` through `6`, with `0` representing Sunday + * - Months are numbered `1` through `12`, while JavaScript `Date` months are + * zero-based + * - `*` normalizes to an empty set internally, so inspect schedules with the + * public helpers instead of assuming every allowed value is stored + * - `next` and `prev` search strictly after or before the provided instant + * - Time-zone-aware schedules account for daylight saving transitions; during + * a fall-back transition, repeated local times are emitted once when moving + * forward + * + * @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: [ + * 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: [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 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 { + return { + tz: cron.tz, + 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) + } +} + +const CronProto = { + [TypeId]: TypeId, + [Equal.symbol](this: Cron, that: unknown) { + return isCron(that) && equals(this, that) + }, + [Hash.symbol](this: Cron): number { + return pipe( + Hash.hash(this.tz), + Hash.combine(Hash.array(Arr.fromIterable(this.seconds))), + Hash.combine(Hash.array(Arr.fromIterable(this.minutes))), + Hash.combine(Hash.array(Arr.fromIterable(this.hours))), + Hash.combine(Hash.array(Arr.fromIterable(this.days))), + Hash.combine(Hash.array(Arr.fromIterable(this.months))), + Hash.combine(Hash.array(Arr.fromIterable(this.weekdays))) + ) + }, + toObject(this: Cron) { + return { + tz: this.tz, + seconds: Arr.fromIterable(this.seconds), + minutes: Arr.fromIterable(this.minutes), + hours: Arr.fromIterable(this.hours), + days: Arr.fromIterable(this.days), + months: Arr.fromIterable(this.months), + weekdays: Arr.fromIterable(this.weekdays) + } + }, + 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 mean + * "match all" for that time unit. + * + * **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: [ + * 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: [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 tz?: DateTime.TimeZone | undefined +}): Cron => { + const o: Mutable = Object.create(CronProto) + o.seconds = new Set(Arr.sort(values.seconds ?? [0], N.Order)) + o.minutes = new Set(Arr.sort(values.minutes, N.Order)) + o.hours = new Set(Arr.sort(values.hours, N.Order)) + o.days = new Set(Arr.sort(values.days, N.Order)) + o.months = new Set(Arr.sort(values.months, N.Order)) + o.weekdays = new Set(Arr.sort(values.weekdays, N.Order)) + o.tz = Option.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: lookupTable(seconds, 60, "next"), + minute: lookupTable(minutes, 60, "next"), + hour: lookupTable(hours, 24, "next"), + day: lookupTable(days, 32, "next"), + month: lookupTable(months, 13, "next"), + weekday: lookupTable(weekdays, 7, "next") + } + + o.prev = { + second: lookupTable(seconds, 60, "prev"), + minute: lookupTable(minutes, 60, "prev"), + hour: lookupTable(hours, 24, "prev"), + day: lookupTable(days, 32, "prev"), + month: lookupTable(months, 13, "prev"), + weekday: lookupTable(weekdays, 7, "prev") + } + + return o +} + +const lookupTable = ( + values: ReadonlyArray, + size: number, + dir: "next" | "prev" +): Array => { + const result = new Array(size).fill(undefined) + if (values.length === 0) { + return result + } + + let current: number | undefined = undefined + + if (dir === "next") { + let index = values.length - 1 + for (let i = size - 1; i >= 0; i--) { + while (index >= 0 && values[index] >= i) { + current = values[index--] + } + result[i] = current + } + } else { + let index = 0 + for (let i = 0; i < size; i++) { + while (index < values.length && values[index] <= i) { + current = values[index++] + } + result[i] = current + } + } + + return result +} + +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 models + * @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`. + * + * **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.split(" ").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(make)) +} + +/** + * Parses a cron expression into a `Cron` instance, throwing on failure. + * + * **When to use** + * + * Use when the input is expected to be valid and you 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** + * + * Seconds, minutes, hours, months, and the optional timezone are checked + * directly. For day constraints, an empty `days` or `weekdays` set means that + * field matches every value; when both sets are non-empty, a date matches if + * either the day-of-month or weekday matches. + * + * **Example** (Matching dates against a schedule) + * + * ```ts + * import { Cron, Result } from "effect" + * + * const cron = Result.getOrThrow(Cron.parse("0 0 4 8-14 * *")) + * + * // 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 utils + * @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.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 * *")) + * + * // 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) // 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 utils + * @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 utils + * @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) { + 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 ? + currentWeekday - 7 + boundary.weekday : + 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 prevMonthDays = daysInMonth(new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), 0))) + b = -(currentDay + (prevMonthDays - boundary.day)) + } else { + b = daysInMonth(current) - currentDay + boundary.day + } + } else { + b = nextDay - currentDay + } + } + + const addDays = 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 => { + if (cron.days.size !== 0) { + return boundary.day + } + const maxDayInMonth = daysInMonth(new Date(Date.UTC(current.getUTCFullYear(), targetMonthIndex + 1, 0))) + return Math.min(boundary.day, maxDayInMonth) + } + if (nextMonth === undefined) { + current.setUTCFullYear(current.getUTCFullYear() + tick) + current.setUTCMonth(boundary.month, clampBoundaryDay(boundary.month)) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + if (needsStep(nextMonth, currentMonth)) { + const targetMonthIndex = nextMonth - 1 + current.setUTCMonth(targetMonthIndex, clampBoundaryDay(targetMonthIndex)) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + } + + return + } + + throw new Error("Unable to find " + direction + " 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")) // 9 AM weekdays + * + * // Get first 5 occurrences + * const iterator = Cron.sequence(cron, new Date("2023-01-01")) + * const next5 = Array.from({ length: 5 }, () => iterator.next().value) + * + * console.log(next5) + * // [Mon Jan 02 2023 09:00:00, Tue Jan 03 2023 09:00:00, ...] + * ``` + * + * @see {@link next} for computing one next occurrence + * + * @category utils + * @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 field restrictions of two `Cron` + * schedules. + * + * **When to use** + * + * Use to compare cron schedules through APIs that accept an equivalence + * relation. + * + * **Details** + * + * This comparison checks seconds, minutes, hours, days, months, and weekdays. + * It does not compare the optional timezone. + * + * **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) => + 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 the same field restrictions. + * + * **When to use** + * + * Use to directly compare whether two cron schedules have the same field + * restrictions. + * + * **Details** + * + * The comparison checks seconds, minutes, hours, days, months, and weekdays. + * It does not compare the optional timezone. + * + * **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 +} + +const secondOptions: SegmentOptions = { + min: 0, + max: 59 +} + +const minuteOptions: SegmentOptions = { + min: 0, + max: 59 +} + +const hourOptions: SegmentOptions = { + min: 0, + max: 23 +} + +const dayOptions: SegmentOptions = { + min: 1, + max: 31 +} + +const monthOptions: SegmentOptions = { + min: 1, + max: 12, + aliases: { + jan: 1, + feb: 2, + mar: 3, + apr: 4, + may: 5, + jun: 6, + jul: 7, + aug: 8, + sep: 9, + oct: 10, + nov: 11, + dec: 12 + } +} + +const weekdayOptions: SegmentOptions = { + min: 0, + max: 6, + aliases: { + sun: 0, + mon: 1, + tue: 2, + wed: 3, + thu: 4, + fri: 5, + sat: 6 + } +} + +const parseSegment = ( + input: string, + options: SegmentOptions +): Result.Result, CronParseError> => { + const capacity = options.max - options.min + 1 + const values = new Set() + const fields = input.split(",") + + for (const field of fields) { + const [raw, step] = splitStep(field) + if (raw === "*" && step === undefined) { + return Result.succeed(new Set()) + } + + 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 ${options.max}`, input })) + } + } + + if (raw === "*") { + for (let i = options.min; i <= options.max; i += step ?? 1) { + values.add(i) + } + } else { + const [left, right] = splitRange(raw, options.aliases) + if (!Number.isInteger(left)) { + return 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) { + values.add(left) + } 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) { + values.add(i) + } + } + } + + if (values.size >= capacity) { + return Result.succeed(new Set()) + } + } + + return Result.succeed(values) +} + +const splitStep = (input: string): [string, number | undefined] => { + const separator = input.indexOf("/") + if (separator !== -1) { + return [input.slice(0, separator), Number(input.slice(separator + 1))] + } + + 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?.[field.toLocaleLowerCase()] ?? Number(field) +} diff --git a/.repos/effect-smol/packages/effect/src/Crypto.ts b/.repos/effect-smol/packages/effect/src/Crypto.ts new file mode 100644 index 00000000000..420f42bc274 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Crypto.ts @@ -0,0 +1,362 @@ +/** + * The `Crypto` module provides a platform-agnostic service for cryptographic + * operations. Runtime packages such as `@effect/platform-node`, + * `@effect/platform-bun`, and `@effect/platform-browser` provide concrete + * implementations backed by the host platform's cryptography APIs. + * + * Use `Crypto` for cryptographic randomness, UUID generation, random values, + * and message digests. The base `Random` service is not cryptographically + * secure unless you replace it with a cryptographically secure implementation. + * + * **Example** (Providing a test Crypto service) + * + * ```ts + * import { Console, 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 id = yield* crypto.randomUUIDv4 + * yield* Console.log(`Created id: ${id}`) + * }) + * + * Effect.runPromise(Effect.provide(program, TestCrypto)) + * ``` + * + * **Example** (Generating random bytes) + * + * ```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 + * return yield* crypto.randomBytes(32) + * }) + * + * Effect.runPromise(Effect.provide(program, TestCrypto)) + * ``` + * + * @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 Crypto service + * from an effect's context. + * + * **Details** + * + * Providing this service supplies the cryptographic operations described by the + * `Crypto` interface. + * + * @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-smol/packages/effect/src/Data.ts b/.repos/effect-smol/packages/effect/src/Data.ts new file mode 100644 index 00000000000..f747ccadb21 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Data.ts @@ -0,0 +1,840 @@ +/** + * Immutable data constructors with discriminated-union support. + * + * The `Data` module provides base classes and factory functions for creating + * immutable value types with a `_tag` field for discriminated unions. + * It is the recommended way to define domain models, error types, and + * lightweight ADTs in Effect applications. + * + * ## Mental model + * + * - **`Class`** — base class for plain immutable data. Extend it with a type + * parameter to declare the fields. Instances are `Pipeable`. + * - **`TaggedClass`** — like `Class` but automatically adds a `readonly _tag` + * string literal field. Useful for single-variant types or ad-hoc tagged + * values. + * - **`TaggedEnum`** (type) + **`taggedEnum`** (value) — define a multi-variant + * discriminated union from a simple record. `taggedEnum()` returns per-variant + * constructors plus `$is` / `$match` helpers. + * - **`Error`** — like `Class` but extends `Cause.YieldableError`, so instances + * can be yielded inside `Effect.gen` to fail the effect. + * - **`TaggedError`** — like `TaggedClass` but extends `Cause.YieldableError`. + * Works with `Effect.catchTag` for tag-based error recovery. + * + * ## Common tasks + * + * - Define a simple value class → {@link Class} + * - Define a value class with a `_tag` → {@link TaggedClass} + * - Define a discriminated union with constructors → {@link TaggedEnum} + {@link taggedEnum} + * - Define a yieldable error → {@link Error} + * - Define a yieldable tagged error → {@link TaggedError} + * - Type-guard a tagged value → `$is` from {@link taggedEnum} + * - Pattern-match on a tagged union → `$match` from {@link taggedEnum} + * + * ## Gotchas + * + * - Variant records passed to `TaggedEnum` must **not** contain a `_tag` key; + * the `_tag` is added automatically from the record key. + * - When a class has no fields, the constructor argument is optional (`void`). + * - `taggedEnum()` creates **plain objects**, not class instances. If you need + * class-based variants, use `TaggedClass` or `TaggedError` instead. + * - `TaggedEnum.WithGenerics` supports up to 4 generic type parameters. + * - `$is(tag)` only checks the `_tag` field, not the full structure. It is safe + * when the tag value is globally unique across your application and the value + * was produced by your constructors. For untrusted input, validate with + * the `Schema` module before using `$is`. + * + * ## Quickstart + * + * **Example** (tagged union with pattern matching) + * + * ```ts + * import { Data } from "effect" + * + * type Shape = Data.TaggedEnum<{ + * Circle: { readonly radius: number } + * Rect: { readonly width: number; readonly height: number } + * }> + * const { Circle, Rect, $match } = Data.taggedEnum() + * + * const area = $match({ + * Circle: ({ radius }) => Math.PI * radius ** 2, + * Rect: ({ width, height }) => width * height + * }) + * + * console.log(area(Circle({ radius: 5 }))) + * // 78.53981633974483 + * console.log(area(Rect({ width: 3, height: 4 }))) + * // 12 + * ``` + * + * @see {@link Class} — plain immutable data class + * @see {@link TaggedEnum} — discriminated union type + * @see {@link taggedEnum} — discriminated union constructors + * @see {@link TaggedError} — yieldable tagged error class + * + * @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. If you also need a `_tag` discriminator, use {@link TaggedClass}; if you need a yieldable error, use {@link Error} or {@link TaggedError}. + * + * **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. For multi-variant unions, prefer {@link TaggedEnum} with {@link taggedEnum}; for yieldable errors, use {@link TaggedError}. + * + * **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. For generic tagged enums, see {@link TaggedEnum.WithGenerics}. + * + * **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`. Pass the interface, not the type alias, to {@link taggedEnum} to get generic-aware constructors and matchers. + * + * **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** (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 to type the constructors-and-matchers object returned by `taggedEnum`. + * + * **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 have a `TaggedEnum` type and need constructors and matchers for its values. For generic enums, pass a {@link TaggedEnum.WithGenerics} interface. + * + * **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** (Basic usage) + * + * ```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** (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 defining yieldable errors that do **not** need tag-based + * discrimination. If you need tag-based recovery, use {@link TaggedError}. + * + * **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 modeling domain errors in Effect applications where you want + * discriminated-union error 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** (Tag-based error recovery) + * + * ```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-smol/packages/effect/src/DateTime.ts b/.repos/effect-smol/packages/effect/src/DateTime.ts new file mode 100644 index 00000000000..cbab7a651fe --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/DateTime.ts @@ -0,0 +1,2935 @@ +/** + * The `DateTime` module provides immutable data types and utilities for working + * with instants, UTC date-times, zoned date-times, and time zones. A + * `DateTime` is always an absolute point in time, represented internally by + * epoch milliseconds, and may also carry a `TimeZone` for zone-aware calendar + * parts and formatting. + * + * **Mental model** + * + * - `DateTime` is a discriminated union: `Utc | Zoned` + * - `Utc` stores an absolute instant without an associated time zone + * - `Zoned` stores the same kind of absolute instant plus a `TimeZone` + * - Time zones can be fixed offsets or named IANA zones such as `"Europe/Rome"` + * - Comparison and ordering use the instant, so two values in different zones + * can still be equivalent + * - Calendar parts and formatted output depend on whether you ask for UTC parts + * or zone-adjusted parts + * + * **Common tasks** + * + * - Construct values: {@link make}, {@link makeUnsafe}, {@link makeZoned}, {@link makeZonedUnsafe} + * - Get the current instant: {@link now}, {@link nowInCurrentZone} + * - Create time zones: {@link zoneMakeOffset}, {@link zoneMakeNamed}, {@link zoneFromString} + * - Attach or change zones: {@link setZone}, {@link setZoneNamed}, {@link setZoneCurrent}, {@link toUtc} + * - Convert to platform values or parts: {@link toDate}, {@link toDateUtc}, {@link toEpochMillis}, {@link toParts}, {@link toPartsUtc} + * - Compare and bound values: {@link Equivalence}, {@link Order}, {@link distance}, {@link min}, {@link max}, {@link clamp}, {@link between} + * - Transform values: {@link add}, {@link subtract}, {@link startOf}, {@link endOf}, {@link nearest}, {@link setParts}, {@link mutate} + * - Format values: {@link format}, {@link formatUtc}, {@link formatLocal}, {@link formatIntl}, {@link formatIso}, {@link formatIsoZoned} + * - Provide an application time zone: {@link CurrentTimeZone}, {@link withCurrentZone}, {@link layerCurrentZone} + * + * **Gotchas** + * + * - `make` and `makeZoned` return `Option`; unsafe constructors throw on invalid + * input + * - `DateTime` equality is instant-based, not display-time-based + * - `setZone` changes the zone used for local parts and formatting without + * changing the represented instant + * - Use `adjustForTimeZone` with {@link makeZoned} when input parts should be + * interpreted as wall-clock time in the target zone + * - Daylight-saving gaps and repeated local times are resolved with + * `Disambiguation` + * - Prefer the Clock-backed {@link now} and `CurrentTimeZone` services in + * Effect workflows; unsafe helpers read from the host environment directly + * + * **See also** + * + * - {@link DateTime} for the UTC/zoned data model + * - {@link TimeZone} for offset and named time-zone values + * - {@link Disambiguation} for daylight-saving ambiguity handling + * + * @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 narrowing 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. + * + * **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. + * + * **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`. + * + * **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. + * + * **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. + * + * **Details** + * + * If the time zone is invalid, it will fail with an `IllegalArgumentError`. + * + * **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. + * + * **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. + * + * **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** (Using 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** (Using 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. + * + * **Details** + * + * It will use the system's local time zone & 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-smol/packages/effect/src/Deferred.ts b/.repos/effect-smol/packages/effect/src/Deferred.ts new file mode 100644 index 00000000000..ecb2c17614e --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Deferred.ts @@ -0,0 +1,967 @@ +/** + * The `Deferred` module provides a one-shot coordination cell for Effect + * programs. A `Deferred` starts empty, can be completed exactly once, and + * allows any number of fibers to wait until that completion is available. + * + * `Deferred` is useful when one fiber must hand a single result, failure, or + * interruption signal to other fibers. Awaiters suspend without blocking an OS + * thread and resume with the same completion once a producer wins the race to + * complete the cell. + * + * **Mental model** + * + * - `Deferred.make` creates an empty cell + * - `Deferred.await` waits for the cell and then observes its stored + * success, failure, defect, or interruption + * - Completion functions such as {@link succeed}, {@link fail}, + * {@link failCause}, {@link interrupt}, and {@link complete} return + * `true` when they complete the cell, or `false` when another fiber already + * completed it + * - Once completed, the result is stable for both current and future awaiters + * + * **Common tasks** + * + * - Create a cell with {@link make} + * - Wait for a result with {@link _await await} + * - Complete with a value or failure using {@link succeed}, {@link fail}, + * {@link failCause}, {@link die}, or {@link interrupt} + * - Complete from another effect using {@link complete} or {@link completeWith} + * - Check completion state with {@link isDone} or inspect it with {@link poll} + * + * **Gotchas** + * + * - A `Deferred` is for a single handoff; use `Queue` when producers emit many + * values over time + * - `complete` runs an effect once and shares its memoized result, while + * `completeWith` stores an effect directly and each awaiter may run it + * - Interrupting a fiber that is waiting on a `Deferred` removes that waiter; + * it does not complete the `Deferred` + * + * **Example** (Opening a start gate) + * + * ```ts + * import { Deferred, Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * const ready = yield* Deferred.make() + * + * const worker = yield* Effect.forkChild( + * Effect.gen(function*() { + * yield* Deferred.await(ready) + * return "started" + * }) + * ) + * + * yield* Deferred.succeed(ready, undefined) + * + * return yield* Fiber.join(worker) + * }) + * ``` + * + * @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 completion 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 utils + * @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 utils + * @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 utils + * @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 utils + * @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 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 utils + * @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 utils + * @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 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 utils + * @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 utils + * @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 utils + * @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 utils + * @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 utils + * @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 utils + * @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 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 utils + * @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-smol/packages/effect/src/Differ.ts b/.repos/effect-smol/packages/effect/src/Differ.ts new file mode 100644 index 00000000000..2a9ab658c0c --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Differ.ts @@ -0,0 +1,63 @@ +/** + * The `Differ` module defines the core abstraction for describing changes to a + * value. A `Differ` knows how to compare two `T` values, produce a + * patch that represents the difference, combine multiple patches, and apply a + * patch to an old value to obtain the updated value. + * + * **Mental model** + * + * - A differ separates "what changed" from "the value after the change" + * - `diff(oldValue, newValue)` produces a `Patch` that can later be applied + * - `patch(oldValue, patch)` replays a patch against a value of the same domain + * - `empty` is the identity patch: applying it should leave the value unchanged + * - `combine(first, second)` composes patches in sequence, where `second` + * represents changes that happen after `first` + * - Patch types are chosen by the differ implementation and may be compact, + * domain-specific, or compatible with a serialization format such as JSON + * Patch + * + * **Common tasks** + * + * - Construct a differ by providing the four operations of the {@link Differ} + * interface + * - Compute a patch with `diff` when you have an old value and a new value + * - Store, transmit, or aggregate patches instead of storing full replacement + * values + * - Combine incremental updates with `combine` before applying them + * - Apply updates with `patch` to reconstruct the next value from a previous + * value and a patch + * + * **Gotchas** + * + * - `combine` is order-sensitive for most patch formats + * - A patch is generally meaningful only for values that belong to the same + * domain and assumptions used by the differ that created it + * - Differs should make `empty` a true identity and should make combined + * patches behave the same as applying the original patches in order + * + * @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-smol/packages/effect/src/Duration.ts b/.repos/effect-smol/packages/effect/src/Duration.ts new file mode 100644 index 00000000000..bf8fca464c6 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Duration.ts @@ -0,0 +1,1846 @@ +/** + * The `Duration` module models spans of time as immutable values with support + * for unit conversion, ordering, arithmetic, and positive or negative infinity. + * It is the standard representation for delays, timeouts, intervals, and + * TTL-like values across Effect APIs. + * + * **Mental model** + * + * - A `Duration` stores either milliseconds, nanoseconds, `Infinity`, or + * `-Infinity`; operations preserve that distinction where it matters + * - Constructor helpers such as {@link seconds} and {@link millis} build + * duration values for the chosen unit + * - {@link Input} values are decoded at API boundaries: numbers mean + * milliseconds, bigints mean nanoseconds, tuples mean `[seconds, nanos]`, + * and strings use units such as `"5 seconds"` + * - Arithmetic and ordering helpers return new duration values rather than + * mutating existing ones + * + * **Common tasks** + * + * - Build durations with {@link nanos}, {@link micros}, {@link millis}, + * {@link seconds}, {@link minutes}, {@link hours}, {@link days}, or + * {@link weeks} + * - Decode flexible inputs with {@link fromInput} or {@link fromInputUnsafe} + * - Convert with {@link toMillis}, {@link toSeconds}, {@link toNanos}, + * {@link toHrTime}, {@link parts}, or {@link format} + * - Compare and constrain durations with {@link Order}, {@link between}, + * {@link min}, {@link max}, and {@link clamp} + * - Combine durations with {@link sum}, {@link subtract}, {@link times}, and + * {@link divide} + * + * **Gotchas** + * + * - Passing a plain number means milliseconds, not seconds + * - {@link toNanosUnsafe} throws for infinite durations; {@link toNanos} + * returns `Option.none()` instead + * - Unsafe decoders and unsafe math helpers throw or apply fallback rules for + * invalid inputs; prefer the safe variants when input is external + * + * **Example** (Decoding and formatting a duration) + * + * ```ts + * import { Duration, Option } from "effect" + * + * const formatted = Duration.fromInput("90 seconds").pipe( + * Option.map((duration) => Duration.format(duration)) + * ) + * ``` + * + * @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 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) + +/** + * 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"`. + * + * @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`. + * + * **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 + const value = Number(valueStr) + switch (unit) { + case "nano": + case "nanos": + return nanos(BigInt(valueStr)) + case "micro": + case "micros": + return micros(BigInt(valueStr)) + case "milli": + case "millis": + return millis(value) + case "second": + case "seconds": + return seconds(value) + case "minute": + case "minutes": + return minutes(value) + case "hour": + case "hours": + return hours(value) + case "day": + case "days": + return days(value) + case "week": + case "weeks": + return weeks(value) + } + 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(BigInt(Math.round(input[0] * 1_000_000_000)) + BigInt(Math.round(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) + let nanos = BigInt(millis) * bigint1e6 + if (obj.microseconds) nanos += BigInt(obj.microseconds) * bigint1e3 + if (obj.nanoseconds) nanos += BigInt(obj.nanoseconds) + return make(nanos) + } + } + 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: BigInt(Math.round(input * 1_000_000)) } + } 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** (Using 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** (Using 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** (Using 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, throwing for infinite durations. + * + * **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 BigInt(Math.round(self.value.millis * 1_000_000)) + } +} + +/** + * 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": { + const n = self.value.nanos + const sign = n < bigint0 ? -BigInt(1) : BigInt(1) + const a = n < bigint0 ? -n : n + return [ + Number(sign * (a / bigint1e9)), + Number(sign * (a % bigint1e9)) + ] + } + case "Millis": { + const m = self.value.millis + const sign = m < 0 ? -1 : 1 + const a = Math.abs(m) + return [ + sign * Math.floor(a / 1000), + sign * Math.round((a % 1000) * 1_000_000) + ] + } + } +} + +/** + * 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 order + * @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 order + * @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`. + * + * **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 these rules: + * + * - infinity - infinity = 0 + * - infinity - negativeInfinity = infinity + * - infinity - finite = infinity + * - negativeInfinity - negativeInfinity = 0 + * - negativeInfinity - infinity = negativeInfinity + * - negativeInfinity - finite = negativeInfinity + * - finite - infinity = negativeInfinity + * - finite - negativeInfinity = 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-smol/packages/effect/src/Effect.ts b/.repos/effect-smol/packages/effect/src/Effect.ts new file mode 100644 index 00000000000..47b41b7cdff --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Effect.ts @@ -0,0 +1,15163 @@ +/** + * `Effect` is the core data type for describing programs that may perform + * synchronous or asynchronous work, fail with typed errors, require services, + * acquire resources, or run concurrently. An `Effect` is lazy: creating + * one describes a workflow, and running it executes that workflow. + * + * **Mental model** + * + * - `A` is the success value, `E` is the expected failure type, and `R` is the + * context of services required to run + * - Effects are immutable descriptions, not promises; composition with + * {@link map}, {@link flatMap}, {@link zip}, or {@link gen} builds a larger + * description + * - Failures in `E` are part of the type signature and can be handled with + * {@link match}, {@link matchEffect}, {@link catchTag}, or {@link catchTags} + * - Requirements in `R` are satisfied before running, usually with + * {@link provide}, {@link provideService}, or layers + * - Fibers are lightweight executions of effects and are used by concurrency + * operators such as {@link all}, {@link race}, {@link forkScoped}, and + * {@link forkDetach} + * + * **Common tasks** + * + * - Create values and failures: {@link succeed}, {@link fail}, {@link failSync} + * - Wrap promise-producing code: {@link tryPromise} + * - Sequence workflows: {@link gen}, {@link flatMap}, {@link map}, {@link tap} + * - Handle errors: {@link match}, {@link matchEffect}, {@link catchTag}, + * {@link catchTags} + * - Run effects at the edge of an application: {@link runPromise}, + * {@link runSync}, {@link runFork} + * - Work with time and interruption: {@link sleep}, {@link timeout}, + * {@link retry} + * - Manage resources: {@link acquireRelease}, {@link scoped}, + * {@link scopedWith} + * - Provide services: {@link provide}, {@link provideContext}, + * {@link provideService}, {@link provideServiceEffect} + * + * **Gotchas** + * + * - Effects do nothing until run by a runtime function such as + * {@link runPromise}, {@link runSync}, or {@link runFork} + * - {@link runSync} is only for effects that can complete synchronously; use + * {@link runPromise} for effects that may suspend asynchronously + * - In {@link gen}, use `yield*` to compose effects; do not use `await` inside + * the generator + * - The `E` type tracks expected failures, not every possible JavaScript + * defect such as an unchecked throw + * - Any remaining `R` requirement must be provided before an effect can be run + * + * **Quickstart** + * + * **Example** (Composing and running a typed workflow) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const divide = (a: number, b: number) => + * b === 0 + * ? Effect.fail("divide by zero") + * : Effect.succeed(a / b) + * + * const program = Effect.gen(function*() { + * const result = yield* divide(10, 2) + * yield* Console.log(`result: ${result}`) + * return result + * }) + * + * Effect.runPromise(program).then(console.log) + * ``` + * + * **See also** + * + * - {@link gen} for generator-based sequencing + * - {@link tryPromise} for asynchronous boundaries + * - {@link acquireRelease} and {@link scoped} for resource safety + * - {@link runPromise}, {@link runSync}, and {@link runFork} for execution + * + * @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 models + * @since 2.0.0 + */ + export type EffectAny = Effect + + /** + * Computes the return type for `Effect.all` when collecting an iterable. + * + * @category models + * @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 models + * @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 models + * @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 models + * @since 2.0.0 + */ + export type IsDiscard = [Extract] extends [ + never + ] ? false + : true + + /** + * Detects whether `Effect.all` should collect results in `Result` mode. + * + * @category models + * @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 models + * @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. + * - `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 creating effects + * @since 2.0.0 + */ +export const promise: ( + evaluate: (signal: AbortSignal) => PromiseLike +) => Effect = internal.promise + +/** + * Creates an `Effect` that represents an asynchronous computation that might + * fail. + * + * **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** + * + * Error Handling: + * + * There are two ways to handle errors with `tryPromise`: + * + * 1. If you don't provide a `catch` function, the error is caught and the + * effect fails with an `UnknownError`. + * 2. If you provide a `catch` function, the error is caught and the `catch` + * function maps it to an error of type `E`. + * + * Interruptions: + * + * An optional `AbortSignal` can be provided to allow for interruption of the + * wrapped `Promise` API. + * + * **Example** (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 creating effects + * @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 you use this function when you need an effect that completes successfully with a + * specific value without any errors or external dependencies. + * + * **Example** (Creating a successful effect) + * + * ```ts + * import { Effect } from "effect" + * + * // Creating an effect that represents a successful scenario + * // + * // ┌─── Effect + * // ▼ + * const success = Effect.succeed(42) + * ``` + * + * @see {@link fail} to create an effect that represents a failure. + * @category creating effects + * @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 creating effects + * @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 creating effects + * @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. This is particularly useful for optimizing expensive computations, managing circular dependencies, or resolving type inference issues. + * + * **Details** + * + * `suspend` takes a thunk that represents the effect and wraps it in a suspended effect. This means the effect will not be created until it is explicitly needed, which is helpful in various scenarios: + * - **Lazy Evaluation**: Helps optimize performance by deferring computations, especially when the effect might not be needed, or when its computation is expensive. This also ensures that any side effects or scoped captures are re-executed on each invocation. + * - **Handling Circular Dependencies**: Useful in managing circular dependencies, such as recursive functions that need to avoid eager evaluation to prevent stack overflow. + * - **Unifying Return Types**: Can help TypeScript unify return types in situations where multiple branches of logic return different effects, simplifying type inference. + * + * **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 creating effects + * @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 are sure the operation will not fail. + * + * **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 creating effects + * @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 creating effects + * @since 2.0.0 + */ + void_ as void +} + +const undefined_: Effect = internal.undefined +export { + /** + * Returns an effect that succeeds with `undefined`. + * + * @category creating effects + * @since 4.0.0 + */ + undefined_ as undefined +} + +/** + * Creates an `Effect` from a callback-based asynchronous API. + * + * **When to use** + * + * Use when integrating 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 creating effects + * @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 creating effects + * @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 `gen` allows you to write code that looks and behaves like synchronous + * code, but it can handle asynchronous tasks, errors, and complex control flow + * (like loops and conditions). It helps make asynchronous code more readable + * and easier to manage. + * + * The generator functions work similarly to `async/await` but with more + * explicit control over the execution of effects. You can `yield*` values from + * effects and return the final result at the end. + * + * **Example** (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 creating effects + * @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 creating effects + * @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 an error in an `Effect`. The error + * will keep propagating unless it is handled. You can handle the error 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 creating effects + * @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 creating effects + * @since 2.0.0 + */ +export const failSync: (evaluate: LazyArg) => Effect = internal.failSync + +/** + * Creates an `Effect` that represents a failure with a specific `Cause`. + * + * **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 creating effects + * @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 creating effects + * @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 encountering unexpected conditions in your code that should + * not be handled as regular errors but instead represent unrecoverable defects. + * + * **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 when 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 creating effects + * @since 2.0.0 + */ +export const die: (defect: unknown) => Effect = internal.die + +const try_: (options: { + try: LazyArg + catch: (error: unknown) => E +}) => Effect = internal.try + +export { + /** + * Creates an `Effect` that represents a synchronous computation that might + * fail. + * + * **When to use** + * + * Use when in situations where you need to perform synchronous operations that might + * fail, such as parsing JSON, you can use the `try` constructor. This + * constructor is designed to handle operations that could throw exceptions by + * capturing those exceptions and transforming them into manageable errors. + * + * **Details** + * + * Error Handling: + * + * There are two ways to handle errors with `try`: + * + * 1. If you don't provide a `catch` function, the error is caught and the + * effect fails with an `UnknownError`. + * 2. If you provide a `catch` function, the error is caught and the `catch` + * function maps it to an error of type `E`. + * + * **Example** (Parsing JSON with typed error mapping) + * + * ```ts + * import { Effect } from "effect" + * + * const parseJSON = (input: string) => + * Effect.try({ + * try: () => JSON.parse(input), + * catch: (error) => error as Error + * }) + * + * // Success case + * Effect.runPromise(parseJSON("{\"name\": \"Alice\"}")).then(console.log) + * // Output: { name: "Alice" } + * + * // Failure case + * Effect.runPromiseExit(parseJSON("invalid json")).then(console.log) + * // Output: Exit.failure with Error + * ``` + * + * **Example** (Mapping synchronous 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 creating effects + * @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 creating effects + * @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 creating effects + * @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 creating effects + * @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`. + * + * **Details** + * + * `Option.some` becomes a successful effect with the contained value, while + * `Option.none` becomes a failed effect with `NoSuchElementError`. + * + * **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) + * + * 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: ( + option: Option +) => Effect = internal.fromOption + +/** + * 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** (Syntax) + * + * ```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 one effect must 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** (Syntax) + * + * ```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 want to perform a side effect, like logging or tracking, + * without modifying the main value. This is useful when you need to observe or + * record an action but want the original value to be passed to the next step. + * + * **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 to handle typed failures as data while preserving + * the original error value. Use `option` when you only care whether the effect + * succeeded, and `exit` when you need the full failure cause. + * + * **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.Err` if the effect fails, containing the recoverable error, or + * `Result.Ok` 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 the failure value is not important and absence is enough. + * Use `result` when you need the original typed failure, and `exit` when you + * need the full failure cause. + * + * **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. Use `result` or `option` when you only + * need to handle typed failures. + * + * **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** (Syntax) + * + * ```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 handle the failure value as a success, or to move the success value + * into the failure channel. + * + * **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. + * + * **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. + * + * **Note**: `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 recovering 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. 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 `catchTag`. 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 a 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 recovery needs 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 use this sparingly, usually at integration boundaries where defects must be + * reported or translated for an external system. + * + * **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. 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 to convert `NoSuchElementError` failures into `Option.none`. + * + * **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 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 only from causes selected by a `Filter`, and the + * recovery needs 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 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 values without changing whether the + * effect 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 a typed failure represents an unrecoverable bug or invalid + * state and should not be handled as a recoverable error. + * + * **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. + * + * **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`, and + * the side effect needs 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 / recursion + * @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 typed failures may be transient, 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. + * + * 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. + * + * **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 strategies, 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 exceeding the time limit should be represented as a typed + * failure. Use `timeoutOption` when a timeout should become `Option.none`, and + * `timeoutOrElse` when you want to run a fallback effect. + * + * **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 should be handled as absence. Use + * `timeout` when a timeout should fail the effect, and `timeoutOrElse` when + * you want to run a fallback effect. + * + * **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 should switch to a fallback effect. Use + * `timeout` when a timeout should fail the effect, and `timeoutOption` when a + * timeout should become `Option.none`. + * + * **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. + * + * **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 to keep only iterable elements accepted by a `Filter` and collect 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 filtering each iterable element requires effects and accepted + * elements should be transformed 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. + * + * **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 an effectful check decides whether to run another effect 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 this is useful for structuring your code to respond differently to success or + * failure 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 and want + * optimal performance for resolved effects. This is particularly useful in + * scenarios where you frequently work with already computed values. + * + * **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 this is useful for differentiating between different types of errors, such as + * regular failures, defects, or interruptions. You can provide specific + * handling logic for each failure type based on the cause. + * + * **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 this is useful when you have effects that are likely to be already resolved + * and you want to avoid the overhead of the effect pipeline. For pending effects, + * it automatically falls back to the regular `matchCause` behavior. + * + * **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 success and cause-aware failure handlers return effects and the + * input may already be resolved, so the selected handler can run immediately + * while unresolved inputs keep normal effectful matching behavior. + * + * **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 both success and failure handling must return effects and the + * failure branch needs the full `Cause`. + * + * **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 the failure or success branch must run additional 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 + +/** + * 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. + * + * **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. + * + * **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 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** (Using a resource 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 use this function when you have an expensive or time-consuming operation that + * you want to avoid repeating. The first evaluation will compute the result, + * and all following evaluations will immediately return the cached value, + * improving performance and reducing unnecessary work. + * + * **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 use this function when you have an effect that involves costly operations or + * computations, and you want to avoid repeating them within a short time frame. + * + * It's ideal for scenarios where the result of an effect doesn't change + * frequently and can be reused for a specified duration. + * + * By caching the result, you can improve efficiency and reduce unnecessary + * computations, especially in performance-critical applications. + * + * **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 / recursion + * @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 / recursion + * @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 / recursion + * @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 / recursion + * @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. + * + * **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 / recursion + * @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 to create an array containing the same effect multiple times when you + * want to pass those effects to another collector or control execution + * separately. + * + * **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. + * + * **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 / recursion + * @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 / recursion + * @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 to execute a typed `Request` through a `RequestResolver` when you want + * concurrent requests made with the same resolver to be collected and completed + * by resolver logic. + * + * **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 effects + * @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 an effect should start in the background and return a fiber that can + * be observed or interrupted. Prefer this when you do not need a `Promise` or + * synchronous result. + * + * **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 effects + * @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. + * + * **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 effects + * @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. + * + * **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 effects + * @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 effects + * @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 effects + * @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. + * + * **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 effects + * @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: + * - If the effect succeeds, the result is wrapped in a `Success`. + * - If it fails, the failure information is provided as a `Failure` containing + * a `Cause` type. + * + * **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 effects + * @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. + * + * **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 effects + * @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 effects + * @since 2.0.0 + */ +export const runSync: (effect: Effect) => A = internal.runSync + +/** + * Executes an effect synchronously with provided services. + * + * **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 effects + * @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: + * - If the effect succeeds, the result is wrapped in a `Success`. + * - If it fails, the failure information is provided as a `Failure` containing + * a `Cause` type. + * + * 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 effects + * @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. + * + * **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 effects + * @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 `Effect.fn` and `Effect.fnUntraced`. + * + * @category models + * @since 3.19.0 + */ + export type Return = Generator, A, any> + + /** + * Type of the untraced function builder used by `Effect.fnUntraced`. + * + * @category models + * @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 models + * @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. + * + * **Details** + * + * `Effect.fnUntraced` also acts as a `pipe` function, so you can append transforms after the body. + * + * **Example** (Defining untraced effect functions) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const greet = Effect.fnUntraced(function* (name: string) { + * yield* Console.log(`Hello, ${name}`) + * return name.length + * }) + * + * Effect.runFork(greet("Ada")) + * ``` + * + * @category functions + * @since 3.12.0 + */ +export const fnUntraced: fn.Untraced = internal.fnUntraced + +/** + * Creates a traced function with an optional span name and `SpanOptionsNoTrace` that adds spans and stack frames, plus pipeable post-processing that receives the Effect and the original arguments. + * + * **Details** + * + * Pipeable functions run after the body and can transform the resulting Effect. + * + * **Example** (Defining traced effect functions) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // Create a named span and post-process the returned Effect. + * const greet = Effect.fn("greet")( + * function*(name: string) { + * yield* Console.log(`Hello, ${name}`) + * return name.length + * }, + * Effect.map((length) => length + 1) + * ) + * + * const program = Effect.gen(function*() { + * const result = yield* greet("Ada") + * yield* Console.log(`Length: ${result}`) + * }) + * ``` + * + * @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. + * + * In Effect transactions are optimistic with retry, that means transactions are retried when: + * + * - the body of the transaction explicitely calls to `Effect.txRetry` and any of the + * accessed transactional values changes. + * + * - any of the accessed transactional values change during the execution of the transaction + * due to a different transaction committing before the current. + * + * 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 utils + * @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** + * + * Behavior: + * + * - For **Success effects**: Applies the mapping function immediately to the value + * - For **Failure effects**: Returns the failure as-is without applying the mapping + * - For **Pending effects**: Falls back to the 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** + * + * Behavior: + * + * - For **Success effects**: Returns the success as-is (no error to transform) + * - For **Failure effects**: Applies the mapping function immediately to the error + * - For **Pending effects**: Falls back to the 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** + * + * Behavior: + * + * - For **Success effects**: Applies the `onSuccess` function immediately to the value + * - For **Failure effects**: Applies the `onFailure` function immediately to the error + * - For **Pending effects**: Falls back to the 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** + * + * Behavior: + * + * - For **Success effects**: Applies the flatMap function immediately to the value + * - For **Failure effects**: Returns the failure as-is without applying the flatMap + * - For **Pending effects**: Falls back to the 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** + * + * Behavior: + * + * - For **Success effects**: Returns the success as-is (no error to catch) + * - For **Failure effects**: Applies the catch function immediately to the error + * - For **Pending effects**: Falls back to the 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-smol/packages/effect/src/Effectable.ts b/.repos/effect-smol/packages/effect/src/Effectable.ts new file mode 100644 index 00000000000..b694425e00f --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Effectable.ts @@ -0,0 +1,98 @@ +/** + * The `Effectable` module provides low-level building blocks for defining + * custom values that behave like `Effect`s. It is primarily used by library + * authors who need domain-specific effect-like data types, such as service + * keys, configuration descriptions, prompts, or other declarative programs + * that can be yielded inside `Effect.gen`. + * + * **Mental model** + * + * - `Effectable` does not run effects by itself; it provides prototypes that + * implement the internal Effect protocol. + * - {@link Prototype} creates a primitive Effect prototype with a custom + * evaluation function that receives the current `Fiber`. + * - {@link Class} is an abstract base class for defining custom classes whose + * instances are also `Effect` values. + * - The success, error, and service requirements of the custom type are + * preserved through the `Effect.Effect` type parameters. + * + * **Common tasks** + * + * - Build an effect-like interface around a declarative data structure. + * - Implement a custom `evaluate` hook that interprets the value in terms of + * the current fiber and returns the underlying `Effect`. + * - Extend {@link Class} when a nominal class-based API is more convenient + * than manually wiring a prototype. + * + * **Gotchas** + * + * - This module is intentionally low-level; most application code should use + * `Effect` constructors and combinators instead. + * - `evaluate` must return an `Effect` with the same success, error, and + * service types as the custom value. + * - Because these APIs participate in the internal Effect protocol, keep + * implementations small and follow existing modules such as `Config` and + * `Context` when adding new effect-like types. + * + * @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-smol/packages/effect/src/Encoding.ts b/.repos/effect-smol/packages/effect/src/Encoding.ts new file mode 100644 index 00000000000..18ee0c90326 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Encoding.ts @@ -0,0 +1,1082 @@ +/** + * Encode and decode text and bytes as Base64, Base64Url, and hexadecimal. + * + * This module covers small, synchronous format conversions where invalid input + * should be reported as data instead of thrown exceptions. Encode functions + * return strings directly; decode functions return `Result.Result` so callers + * can branch on success or inspect an {@link EncodingError}. + * + * **Mental model** + * + * String inputs are first converted to UTF-8 bytes with `TextEncoder`. + * `Uint8Array` inputs are encoded directly. Byte decoders return raw + * `Uint8Array` values, while `*String` decoders decode the resulting bytes as + * UTF-8 text with `TextDecoder`. + * + * **Common tasks** + * + * - Use {@link encodeBase64} and {@link decodeBase64} for standard padded RFC + * 4648 Base64 + * - Use {@link encodeBase64Url} and {@link decodeBase64Url} for unpadded + * URL-safe Base64 + * - Use {@link encodeHex} and {@link decodeHex} for lowercase hexadecimal + * - Use the `*String` decoders when the encoded data represents UTF-8 text + * - Use {@link isEncodingError} to recognize failures returned by decode + * operations + * + * **Gotchas** + * + * - Decode functions do not throw for malformed input; they return + * `Result.fail` with an `EncodingError`. + * - Base64 decoders ignore carriage returns and line feeds before validation. + * - {@link decodeBase64Url} accepts padded and unpadded URL-safe input, but + * {@link encodeBase64Url} emits unpadded output. + * - Hex encoding emits lowercase letters, and hex decoding requires an even + * number of hexadecimal characters. + * + * **Example** (Decode Base64 without throwing) + * + * ```ts + * import { Encoding, Result } from "effect" + * + * const decoded = Encoding.decodeBase64String("aGVsbG8=") + * + * if (Result.isSuccess(decoded)) { + * console.log(decoded.success) + * } + * ``` + * + * @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`. + * + * **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-smol/packages/effect/src/Equal.ts b/.repos/effect-smol/packages/effect/src/Equal.ts new file mode 100644 index 00000000000..37027161f76 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Equal.ts @@ -0,0 +1,623 @@ +/** + * Structural and custom equality for Effect values. + * + * The `Equal` module provides deep structural comparison for primitives, plain + * objects, arrays, Maps, Sets, Dates, and RegExps. Types that implement the + * {@link Equal} interface can supply their own comparison logic while staying + * compatible with the rest of the ecosystem (HashMap, HashSet, etc.). + * + * ## Mental model + * + * - **Structural equality** — two values are equal when their contents match, + * not when they share the same reference. + * - **Hash-first shortcut** — before comparing fields, the module checks + * {@link Hash.hash}. If the hashes differ the objects are unequal without + * further traversal. + * - **Equal interface** — any object that implements both {@link symbol} (the + * equality method) and `Hash.symbol` (the hash method) can define custom + * comparison logic. + * - **Caching** — comparison results for object pairs are cached in a WeakMap. + * This makes repeated checks fast but **requires immutability** after the + * first comparison. + * - **By-reference opt-out** — {@link byReference} and {@link byReferenceUnsafe} + * let you switch individual objects back to reference equality when you need + * mutable identity semantics. + * + * ## Common tasks + * + * - Compare two values → {@link equals} + * - Check if a value implements `Equal` → {@link isEqual} + * - Use `equals` where an `Equivalence` is expected → {@link asEquivalence} + * - Implement custom equality on a class → implement {@link Equal} (see + * example on the interface) + * - Opt an object out of structural equality → {@link byReference} / + * {@link byReferenceUnsafe} + * + * ## Gotchas + * + * - Objects **must be treated as immutable** after their first equality check. + * Results are cached; mutating an object afterwards yields stale results. + * - `NaN` is considered equal to `NaN` (unlike `===`). + * - Functions without an `Equal` implementation are compared by reference. + * - Map and Set comparisons are order-independent but O(n²) in size. + * - If only one of two objects implements `Equal`, they are never equal. + * + * ## Quickstart + * + * **Example** (basic structural comparison) + * + * ```ts + * import { Equal } from "effect" + * + * // Primitives + * console.log(Equal.equals(1, 1)) // true + * console.log(Equal.equals("a", "b")) // false + * + * // Objects and arrays + * console.log(Equal.equals({ x: 1 }, { x: 1 })) // true + * console.log(Equal.equals([1, 2], [1, 2])) // true + * + * // Curried form + * const is42 = Equal.equals(42) + * console.log(is42(42)) // true + * console.log(is42(0)) // false + * ``` + * + * @see {@link equals} — the main comparison function + * @see {@link Equal} — the interface for custom equality + * @see {@link Hash} — the companion hashing module + * + * @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 use it as the computed property key when implementing custom equality on a + * class or object literal. + * - Use it to check manually whether an object carries an equality method (prefer + * {@link isEqual} instead). + * + * **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 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** (Coordinate with Value Equality) + * + * ```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 as the default equality check throughout Effect code. + * - In data-level assertions or conditional logic where structural comparison + * is needed. + * - In its curried (single-argument) form to build reusable predicates. + * + * **Details** + * + * - Returns a `boolean`; never throws. + * - Primitives: compared by value. `NaN` equals `NaN`. + * - Objects implementing {@link Equal}: delegates to their + * `[Equal.symbol]` method. If only one operand implements `Equal`, the + * result is `false`. + * - Dates: compared by ISO string representation. + * - RegExps: compared by string representation. + * - Arrays: element-by-element recursive comparison (order matters). + * - Maps / Sets: structural comparison of entries (order-independent). + * - Plain objects: all own and inherited enumerable keys are compared + * recursively. + * - Functions without an `Equal` implementation are compared by reference. + * - Circular references are handled; two structures that are circular at the + * same depth are considered equal. + * - Hash values are checked first as a fast-path rejection. + * - Supports dual (data-last) usage: call 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 to branch on whether a value supports custom equality before calling + * its `[Equal.symbol]` method directly. + * - In generic utility code that needs to distinguish `Equal` implementors + * from plain values. + * + * **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** (Type Guard) + * + * ```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 when an API (e.g. `Array.dedupeWith`, `Equivalence.mapInput`) requires an + * `Equivalence` and you want to reuse `Equal.equals`. + * + * **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 when you have a plain object or array that should be compared by identity + * (reference), not by contents. + * - When you want to preserve the original object unchanged and get a new + * reference-equal handle. + * + * **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 utility + * @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 when you want reference equality semantics and can accept that the + * original object is **permanently** modified. + * - When proxy overhead is unacceptable (hot paths, large collections). + * + * **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 utility + * @since 4.0.0 + */ +export const byReferenceUnsafe = (obj: T): T => { + byReferenceInstances.add(obj) + return obj +} diff --git a/.repos/effect-smol/packages/effect/src/Equivalence.ts b/.repos/effect-smol/packages/effect/src/Equivalence.ts new file mode 100644 index 00000000000..bc6b0e70193 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Equivalence.ts @@ -0,0 +1,936 @@ +/** + * Utilities for defining equivalence relations - binary relations that determine when two values + * should be considered equivalent. Equivalence relations are used for comparing, deduplicating, + * and organizing data in collections and data structures. + * + * ## Mental model + * + * - **Equivalence relation**: A function `(a: A, b: A) => boolean` that returns `true` when values are equivalent + * - **Reflexive property**: Every value is equivalent to itself (`eq(a, a) === true`) + * - **Symmetric property**: If `a` is equivalent to `b`, then `b` is equivalent to `a` (`eq(a, b) === eq(b, a)`) + * - **Transitive property**: If `a` is equivalent to `b` and `b` is equivalent to `c`, then `a` is equivalent to `c` + * - **Reference equality optimization**: {@link make} checks `===` first for performance before calling the custom function + * - **Composition**: Equivalences can be combined using {@link combine} and {@link combineAll} to create more complex relations + * + * ## Common tasks + * + * - Creating custom equivalences → {@link make} + * - Using strict equality (`===`) → {@link strictEqual} + * - Combining multiple equivalences (AND logic) → {@link combine}, {@link combineAll} + * - Transforming input before comparison → {@link mapInput} + * - Creating equivalences for structured types → {@link Struct}, {@link Tuple}, {@link Array_}, {@link Record} + * + * ## Gotchas + * + * - `strictEqual` uses `===`, so `NaN !== NaN` and objects are compared by reference, not structure + * - `make` optimizes with a reference equality check, so identical references return `true` without calling the function + * - `combineAll` with an empty collection returns an equivalence that always returns `true` + * - `Tuple` and `Array` require matching lengths; different lengths are never equivalent + * + * ## Quickstart + * + * **Example** (Case-insensitive string equivalence) + * + * ```ts + * import { Array, Equivalence } from "effect" + * + * const caseInsensitive = Equivalence.make((a, b) => + * a.toLowerCase() === b.toLowerCase() + * ) + * + * const strings = ["Hello", "world", "HELLO", "World"] + * const deduplicated = Array.dedupeWith(strings, caseInsensitive) + * console.log(deduplicated) // ["Hello", "world"] + * ``` + * + * ## See also + * + * - `Equal` - For structural equality (can convert to Equivalence) + * - `Array.dedupeWith` - Remove duplicates using an equivalence + * - `Chunk` - Collections that use equivalences for operations + * + * @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 for equivalence functions + * - Use when implementing custom equivalence logic + * - Use when working with collection operations that require equivalence relations + * + * **Details** + * + * - Returns `boolean`: `true` if values are equivalent, `false` otherwise + * - Must satisfy reflexive, symmetric, and transitive properties + * + * **Example** (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** (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 rarely needed in application code + * - Use primarily for internal type system operations and HKT (Higher-Kinded Types) abstractions + * - Use when working with generic type constructors that require type lambdas + * + * **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 a custom equivalence that is not just strict equality + * - Use when creating equivalences for complex types with custom comparison logic + * - Use when you want the performance benefit of reference equality optimization + * + * **Details** + * + * - First checks reference equality (`===`) for performance; if values are identical, returns `true` without calling the function + * - Falls back to the provided equivalence function if values are not the same reference + * - The provided function 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** (Numeric tolerance equivalence) + * + * ```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 primitive types where `===` is appropriate + * - Use when you need reference equality for objects + * - Use as a building block for more complex equivalences via {@link mapInput} or {@link combine} + * - Use when performance is critical and you do not need structural equality + * + * **Details** + * + * - Uses JavaScript's strict equality operator (`===`) + * - For primitives: compares values directly + * - For objects: compares by reference, so only the same object instance is equivalent + * + * **Gotchas** + * + * `NaN !== NaN`, so `NaN` values are never considered equivalent. + * + * **Example** (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** (Reference equality for objects) + * + * ```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 an API needs an `Equivalence` instance for 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 an API needs an `Equivalence` instance for numeric equality where + * `NaN` equals `NaN`. + * + * **Details** + * + * `NaN` is considered equal to `NaN`. + * + * **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 an API needs an `Equivalence` instance for 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 an API needs an `Equivalence` instance for `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 + * - Use when building complex equivalences from simpler ones + * - Use when you want both conditions to be satisfied + * + * **Details** + * + * - Returns `true` only if both equivalences return `true` + * - Short-circuits: if the first equivalence returns `false`, the second is not called + * - 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 three or more equivalences + * - Use when you have a dynamic collection of equivalences to combine + * - Use when building equivalences from arrays or iterables + * - Prefer this over multiple `combine` calls when you have many equivalences + * + * **Details** + * + * - Returns `true` only if all equivalences in the collection return `true` + * - Short-circuits: 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** (Empty collection edge case) + * + * ```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 a complex type based on a single property + * - Use when you want to normalize values before comparison, such as case-insensitive strings + * - Use when creating equivalences that focus on specific fields of objects + * - Use as a building block for creating equivalences via {@link combine} or {@link combineAll} + * + * **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 + * + * **Example** (Equivalence based on 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 comparing tuples with different types at each position + * - Use when you need different equivalence logic for each tuple element + * - Use when working with fixed-length tuples instead of arrays + * - Prefer this over `Array` when you have a known tuple structure with different types + * + * **Details** + * + * - Requires tuples to have the same length; different lengths are never equivalent + * - Applies each equivalence to the corresponding element position + * - Returns `true` only if all elements are equivalent according to their respective equivalences + * - The result is also an equivalence that satisfies reflexive, symmetric, and transitive properties + * + * **Example** (Homogeneous tuple equivalence) + * + * ```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** (Tuple 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 comparing arrays with homogeneous element types + * - Use when all elements should use the same equivalence logic + * - Use when working with variable-length arrays instead of fixed tuples + * - Prefer this over `Tuple` when you have arrays of the same type + * + * **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** (Number array equivalence) + * + * ```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 comparing objects with known, fixed property names + * - Use when you need different equivalence logic for different properties + * - Use when working with struct or interface types with specific fields + * - Prefer this over `Record` when you have a fixed set of known properties + * + * **Details** + * + * - Compares only the properties specified in the struct definition + * - Properties not in the struct are ignored + * - Returns `true` only if all specified properties are equivalent according to their equivalences + * - Supports both string and symbol keys via `Reflect.ownKeys` + * - The result is also an equivalence that satisfies reflexive, symmetric, and transitive properties + * + * **Example** (Struct 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** (Partial equivalence for 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 comparing objects with dynamic or unknown property names + * - Use when all property values should use the same equivalence logic + * - Use when working with record or dictionary types + * - Prefer this over `Struct` when you have variable properties or need to compare all properties uniformly + * + * **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** (Record 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** (Record 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 to combine multiple equivalences from a collection using reducer patterns + * - Use when implementing fold operations over collections of equivalences + * - Use when working with reducers that operate on equivalences + * + * **Details** + * + * - Returns a reducer that combines equivalences using {@link combine} + * - Uses an equivalence that always returns `true` as the identity element for empty collections + * - Uses {@link combineAll} for combining collections of equivalences + * - The reducer can be used with fold operations on collections + * + * **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 utils + * @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 comparing `Date` values 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** (Reference vs 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-smol/packages/effect/src/ErrorReporter.ts b/.repos/effect-smol/packages/effect/src/ErrorReporter.ts new file mode 100644 index 00000000000..19a7d44a38d --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/ErrorReporter.ts @@ -0,0 +1,582 @@ +/** + * Pluggable error reporting for Effect programs. + * + * Reporting is triggered by `Effect.withErrorReporting`, + * `ErrorReporter.report`, or built-in reporting boundaries in the HTTP and + * RPC server modules. + * + * Each reporter receives a structured callback with the failing `Cause`, a + * pretty-printed `Error`, severity, and any extra attributes attached to the + * original error — making it straightforward to forward failures to Sentry, + * Datadog, or a custom logging backend. + * + * Use the annotation symbols (`ignore`, `severity`, `attributes`) on your + * error classes to control reporting behavior per-error. + * + * **Example** (Reporting errors with annotations) + * + * ```ts + * import { Data, Effect, ErrorReporter } from "effect" + * + * // A reporter that logs to the console + * const consoleReporter = ErrorReporter.make(({ error, severity }) => { + * console.error(`[${severity}]`, error.message) + * }) + * + * // An error that should be ignored by reporters + * class NotFoundError extends Data.TaggedError("NotFoundError")<{}> { + * readonly [ErrorReporter.ignore] = true + * } + * + * // An error with custom severity and attributes + * class RateLimitError extends Data.TaggedError("RateLimitError")<{ + * readonly retryAfter: number + * }> { + * readonly [ErrorReporter.severity] = "Warn" as const + * readonly [ErrorReporter.attributes] = { + * retryAfter: this.retryAfter + * } + * } + * + * // Opt in to error reporting with Effect.withErrorReporting + * const program = Effect.gen(function*() { + * return yield* new RateLimitError({ retryAfter: 60 }) + * }).pipe( + * Effect.withErrorReporting, + * Effect.provide(ErrorReporter.layer([consoleReporter])) + * ) + * ``` + * + * @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 low-level code needs to read or replace the current set of 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-smol/packages/effect/src/ExecutionPlan.ts b/.repos/effect-smol/packages/effect/src/ExecutionPlan.ts new file mode 100644 index 00000000000..daf8239b7b4 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/ExecutionPlan.ts @@ -0,0 +1,489 @@ +/** + * Defines finite fallback plans for effects and streams that should retry the + * same work with different services. An `ExecutionPlan` is a non-empty + * sequence of steps. Each step provides a `Context` or `Layer`, and can limit + * or shape retries with `attempts`, `schedule`, and `while`. + * + * Use a plan with `Effect.withExecutionPlan` or `Stream.withExecutionPlan`. + * Those APIs run the wrapped work with the first step, retry within that step + * as directed, then move to later steps until the work succeeds or the plan is + * exhausted. + * + * **Mental model** + * + * - A step is a resource profile for one part of a fallback strategy + * - The active step supplies the services visible to the wrapped effect or + * stream + * - `attempts` caps the number of tries for a step + * - `schedule` controls retry timing and receives the wrapped work's failure + * value + * - `while` can stop the current step early by inspecting the same failure + * value + * - `CurrentMetadata` exposes the 1-based attempt number and 0-based step + * index to code running under the active step + * + * **Common tasks** + * + * - Build a plan with {@link make} + * - Append fallback plans with {@link merge} + * - Apply a plan with `Effect.withExecutionPlan` or `Stream.withExecutionPlan` + * - Read active step and attempt information with {@link CurrentMetadata} + * - Carry the current environment into a plan with `captureRequirements` + * + * **Quickstart** + * + * **Example** (Retry with one layer, then fall back) + * + * ```ts + * import { Context, Effect, ExecutionPlan, Layer, Schedule } from "effect" + * + * class Endpoint extends Context.Service()("Endpoint", { + * make: Effect.succeed({ url: "primary" }) + * }) {} + * + * const primary = Layer.succeed(Endpoint, Endpoint.of({ url: "primary" })) + * const fallback = Layer.succeed(Endpoint, Endpoint.of({ url: "fallback" })) + * + * const plan = ExecutionPlan.make( + * { + * provide: primary, + * attempts: 2, + * schedule: Schedule.recurs(1) + * }, + * { + * provide: fallback + * } + * ) + * + * const program = Effect.gen(function*() { + * const endpoint = yield* Endpoint + * if (endpoint.url === "primary") { + * return yield* Effect.fail("unavailable") + * } + * return endpoint.url + * }).pipe(Effect.withExecutionPlan(plan)) + * ``` + * + * **Gotchas** + * + * - Plans must contain at least one step + * - `attempts` must be greater than zero when provided + * - Without `attempts` or `schedule`, a step is tried once + * - A `schedule` can keep a step active until bounded by `attempts` or + * stopped by `while` + * - Layer, schedule, and predicate requirements stay in the plan type until + * they are provided or captured + * + * **See also** + * + * - {@link make} for constructing plans + * - {@link merge} for combining plans in order + * - {@link CurrentMetadata} for inspecting the active 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-smol/packages/effect/src/Exit.ts b/.repos/effect-smol/packages/effect/src/Exit.ts new file mode 100644 index 00000000000..c20360b1e16 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Exit.ts @@ -0,0 +1,1139 @@ +/** + * Represents the outcome of an Effect computation as a plain, synchronously + * inspectable value. + * + * ## Mental model + * + * - `Exit` is a union of two cases: `Success` and `Failure` + * - A `Success` wraps a value of type `A` + * - A `Failure` wraps a `Cause`, which may contain typed errors, defects, or interruptions + * - `Exit` is also an `Effect`, so you can yield it directly inside `Effect.gen` + * - Constructors mirror the failure modes: {@link fail} for typed errors, {@link die} for defects, {@link interrupt} for fiber interruptions + * - Use `Exit` when you need to inspect an Effect result without running further effects + * + * ## Common tasks + * + * - Create a success: {@link succeed} + * - Create a typed failure: {@link fail} + * - Create a failure from a Cause: {@link failCause} + * - Create a defect: {@link die} + * - Create an interruption: {@link interrupt} + * - Check the outcome: {@link isSuccess}, {@link isFailure}, {@link match} + * - Extract values optionally: {@link getSuccess}, {@link getCause}, {@link findErrorOption} + * - Transform the result: {@link map}, {@link mapError}, {@link mapBoth} + * - Combine multiple exits: {@link asVoidAll} + * - Inspect failure categories: {@link hasFails}, {@link hasDies}, {@link hasInterrupts} + * + * ## Gotchas + * + * - A `Failure` wraps a `Cause`, not a bare `E`. Use Cause utilities to drill into it. + * - {@link mapError} and {@link mapBoth} only transform typed errors (Fail reasons in the Cause). If the Cause contains only defects or interruptions, the original failure passes through unchanged. + * - Filter-based APIs ({@link filterSuccess}, {@link filterValue}, etc.) return `Result.fail` values for pipeline composition. They are not `Option` values or Effect failures. + * - {@link findError} and {@link findDefect} return only the first matching reason from the Cause. + * + * ## Quickstart + * + * **Example** (Creating and inspecting exits) + * + * ```ts + * import { Exit } from "effect" + * + * const success = Exit.succeed(42) + * const failure = Exit.fail("not found") + * + * const message = Exit.match(success, { + * onSuccess: (value) => `Got: ${value}`, + * onFailure: () => "Failed" + * }) + * console.log(message) // "Got: 42" + * ``` + * + * ## See also + * + * - {@link Exit} the core union type + * - {@link succeed} and {@link fail} the most common constructors + * - {@link match} for pattern matching on an Exit + * + * @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` + * - `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 + * - Works as a type guard, narrowing 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 to wrap a known success value into an Exit + * - Use when constructing test data or returning explicit results + * + * **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 + * - Use 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 expected, recoverable failures + * + * **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 success Exit but do not care about the value + * - Avoids allocating a new Exit for a common case + * + * **Details** + * + * Equivalent to `Exit.succeed(undefined)` but shared as a single instance. + * + * **Example** (Using 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` + * - After narrowing, the `value` property becomes accessible + * + * **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` + * - After narrowing, the `cause` property becomes accessible + * + * **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 for 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 if a fiber was interrupted + * + * **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 composing Exit checks with `Filter` or other `Result`-based + * filtering APIs and you only need the first typed error in the Cause. + * + * **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 composing Exit checks with `Filter` or other `Result`-based + * filtering APIs and you only need the first defect in the Cause. + * + * **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 outcomes + * + * **Details** + * + * - Calls `onSuccess` with the value if the Exit is a Success + * - Calls `onFailure` with the Cause if the Exit is a Failure + * + * Supports both curried and direct call styles (data-last and data-first). + * + * **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** + * + * - Has no effect on failures, which pass through unchanged + * + * Allocates a new Exit if successful. + * Supports both curried and direct call styles. + * + * **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** + * + * - Has no effect on successes, which pass through unchanged + * + * Allocates a new Exit if the error is transformed. + * Supports both curried and direct call styles. + * + * **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. + * Supports both curried and direct call styles. + * + * **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 only care about whether the computation succeeded or failed, not the value + * + * **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, returns a void success + * - If any exit is a failure, 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 want to optionally extract the value without pattern matching + * + * **Details** + * + * - Returns `Option.some(value)` for a Success, `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 want to optionally inspect the failure cause + * + * **Details** + * + * - Returns `Option.some(cause)` for a Failure, `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 want to optionally extract a typed error without dealing with the full Cause + * + * **Details** + * + * - Returns `Option.some(error)` if the Cause contains a Fail reason, `Option.none()` otherwise + * - Returns `Option.none()` for successes, defect-only failures, and interrupt-only failures + * + * **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-smol/packages/effect/src/Fiber.ts b/.repos/effect-smol/packages/effect/src/Fiber.ts new file mode 100644 index 00000000000..5d773184637 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Fiber.ts @@ -0,0 +1,677 @@ +/** + * The `Fiber` module provides operations for handles returned by forking + * effects. A `Fiber` is a lightweight runtime execution of an `Effect` + * that may still be running, may already have completed, and can be observed or + * interrupted by other fibers. + * + * Applications create fibers with operations such as `Effect.forkChild`, + * `Effect.forkScoped`, `Effect.forkIn`, or `Effect.forkDetach`, then use this + * module to wait for results, inspect exits, or cancel work that is no longer + * needed. + * + * **Mental model** + * + * - A fiber is a handle to a running or completed effect, not the effect itself + * - {@link await_ await} observes completion as an `Exit` without failing the current + * effect + * - {@link join} waits for success and propagates the fiber's failure into the + * current effect + * - {@link interrupt} requests cancellation and waits until the target fiber + * finishes running finalizers + * - Group operations such as {@link awaitAll}, {@link joinAll}, and + * {@link interruptAll} apply the same ideas to collections of fibers + * + * **Common tasks** + * + * - Wait for one fiber with {@link await_ await} or {@link join} + * - Wait for many fibers with {@link awaitAll} or {@link joinAll} + * - Stop work with {@link interrupt}, {@link interruptAs}, + * {@link interruptAll}, or {@link interruptAllAs} + * - Recognize fiber handles with {@link isFiber} + * - Link a manually managed fiber to a `Scope` with {@link runIn} + * + * **Gotchas** + * + * - `await` gives you an `Exit`; use `join` when the current effect should fail + * if the fiber failed + * - Interruption is cooperative, so a fiber can continue through + * uninterruptible regions and finalizers before it finishes + * - `joinAll` stops waiting on the first failure, but it does not interrupt the + * remaining fibers for you + * + * **Example** (Joining a forked effect) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * const fiber = yield* Effect.forkChild(Effect.succeed(42)) + * const value = yield* Fiber.join(fiber) + * + * return value + * }) + * ``` + * + * @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 the forked fiber is part of the current workflow and + * its failure should fail the current Effect. + * + * **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 every fiber must succeed and you 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 a forked fiber is no longer needed and should be cancelled. + * + * **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 a group of forked fibers is no longer needed. + * + * **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-smol/packages/effect/src/FiberHandle.ts b/.repos/effect-smol/packages/effect/src/FiberHandle.ts new file mode 100644 index 00000000000..9b403f3ee40 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/FiberHandle.ts @@ -0,0 +1,830 @@ +/** + * The `FiberHandle` module provides a scoped handle for managing the lifecycle + * of at most one fiber at a time. A `FiberHandle` can hold one + * `Fiber`; when a new fiber is installed, the previous fiber is + * interrupted unless the operation is configured with `onlyIfMissing`. + * + * **Mental model** + * + * - A handle is either open with zero or one current fiber, or closed by its + * surrounding `Scope` + * - Closing the scope interrupts the current fiber and prevents new work from + * being accepted + * - Completed fibers remove themselves from the handle, so the handle can be + * reused for later work + * - Replacing a fiber uses the handle's internal interruption id, allowing + * expected replacement interruptions to be distinguished from real failures + * + * **Common tasks** + * + * - Create a scoped handle: {@link make} + * - Fork an effect into the handle: {@link run} + * - Store an existing fiber: {@link set} + * - Read or clear the current fiber: {@link get}, {@link clear} + * - Capture runtime-specific runners: {@link makeRuntime}, {@link runtime} + * - Run handled effects as Promises: {@link makeRuntimePromise}, + * {@link runtimePromise} + * - Wait for failure or closure: {@link join} + * - Wait until the current fiber is gone: {@link awaitEmpty} + * + * **Gotchas** + * + * - The handle never contains more than one live fiber; starting or setting + * another fiber interrupts the previous one by default + * - Use `onlyIfMissing` when a call should leave an already running fiber in + * place instead of replacing it + * - `join` observes the handle's failure/close signal; successful fiber + * completion only empties the handle + * - `awaitEmpty` waits for the fiber that is current when it starts; later + * calls to {@link run} or {@link set} can install new work + * + * @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. + * + * **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. + * + * **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. + * + * **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-smol/packages/effect/src/FiberMap.ts b/.repos/effect-smol/packages/effect/src/FiberMap.ts new file mode 100644 index 00000000000..595628ff386 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/FiberMap.ts @@ -0,0 +1,1042 @@ +/** + * The `FiberMap` module provides a scoped, mutable collection for managing + * fibers by key. A `FiberMap` owns a set of running fibers, interrupts + * them when its scope closes, and automatically removes each entry when the + * corresponding fiber completes. + * + * **Mental model** + * + * - A `FiberMap` is a keyed registry of fibers with lifecycle management + * - Keys identify the currently active fiber for a logical task or resource + * - Adding a fiber under an existing key interrupts the previous fiber by default + * - Completed fibers remove themselves from the map if they are still current + * - Closing the map's scope interrupts every fiber that remains in the map + * - The map can surface the first non-ignored managed fiber failure via {@link join} + * + * **Common tasks** + * + * - Create a scoped map: {@link make} + * - Fork effects into the map: {@link run} + * - Add existing fibers: {@link set} + * - Create captured runners: {@link makeRuntime}, {@link runtime} + * - Bridge to Promise-based callers: {@link makeRuntimePromise}, {@link runtimePromise} + * - Inspect entries: {@link get}, {@link has}, {@link size} + * - Stop work: {@link remove}, {@link clear} + * - Coordinate completion or failure: {@link awaitEmpty}, {@link join} + * + * **Gotchas** + * + * - `FiberMap` is scoped; use it with `Effect.scoped` or another scope owner so + * managed fibers are interrupted when the scope closes + * - Reusing a key is a replacement operation unless `onlyIfMissing` is enabled + * - `join` waits for the map to fail or close; use {@link awaitEmpty} to wait + * until all currently managed fibers have completed + * - The `Unsafe` variants mutate synchronously and should only be used when the + * caller already controls the surrounding execution context + * + * @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. + * + * **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. + * + * **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. + * + * **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-smol/packages/effect/src/FiberSet.ts b/.repos/effect-smol/packages/effect/src/FiberSet.ts new file mode 100644 index 00000000000..85a3a2992db --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/FiberSet.ts @@ -0,0 +1,733 @@ +/** + * The `FiberSet` module provides a scoped container for managing many fibers as + * one lifecycle. A `FiberSet` tracks fibers whose successful values are + * compatible with `A` and whose failures are compatible with `E`, removes each + * fiber when it completes, and interrupts all still-running fibers when the + * owning `Scope` closes. + * + * **Mental model** + * + * - A `FiberSet` is an owned, scoped collection of fibers + * - Fibers can be added directly with {@link add} / {@link addUnsafe} + * - Effects can be forked into the set with {@link run}, {@link runtime}, or + * {@link runtimePromise} + * - Completed fibers are automatically removed from the set + * - Closing the scope or calling {@link clear} interrupts the currently tracked + * fibers + * - {@link join} waits for the set's first non-ignored failure, while + * {@link awaitEmpty} waits until all tracked fibers have completed + * + * **Common tasks** + * + * - Create a scoped set: {@link make} + * - Create scoped runners: {@link makeRuntime}, {@link makeRuntimePromise} + * - Add an existing fiber: {@link add} + * - Fork an effect into the set: {@link run} + * - Interrupt tracked fibers: {@link clear} + * - Observe the set: {@link size}, {@link awaitEmpty}, {@link join} + * - Check a value: {@link isFiberSet} + * + * **Gotchas** + * + * - `FiberSet` values are scoped; use them inside `Effect.scoped` or another + * scope owner so their fibers are interrupted reliably + * - Adding or running into a closed set interrupts the fiber immediately + * - By default, interruptions are not treated as failures for {@link join}; + * use the `propagateInterruption` option when interruption should be + * propagated + * + * @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. + * + * **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. + * + * **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-smol/packages/effect/src/FileSystem.ts b/.repos/effect-smol/packages/effect/src/FileSystem.ts new file mode 100644 index 00000000000..2a7dcc69ce3 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/FileSystem.ts @@ -0,0 +1,1437 @@ +/** + * Effect service for reading, writing, inspecting, and watching files. + * + * The `FileSystem` service is the portable boundary between Effect programs and + * the host file system. Programs depend on the service from `effect/FileSystem`; + * platform packages provide concrete layers at the edge. Operations return + * `Effect`, `Stream`, or `Sink` values and report failures as `PlatformError` + * instead of throwing. + * + * **Mental model** + * + * `FileSystem` is a capability, not a global singleton. Request it with + * `yield* FileSystem.FileSystem` inside an effect, compose file work like any + * other effect, and provide a platform or test implementation when the program + * is run. Scoped operations such as `open`, `makeTempFileScoped`, and + * `makeTempDirectoryScoped` bind resource cleanup to `Scope`. + * + * **Common tasks** + * + * - Create, copy, rename, remove, chmod, chown, link, and symlink paths. + * - Read and write whole files as bytes or strings. + * - Stream large files with `stream` and `sink`, using binary size helpers such + * as `KiB` and `MiB` for chunk sizes and offsets. + * - Inspect metadata with `stat`, check accessibility with `access` or + * `exists`, and canonicalize paths with `realPath`. + * - Watch files or directories with `watch` when the platform implementation + * supports it. + * + * **Example** (Write and clean up a temporary file) + * + * ```ts + * import { Effect, FileSystem } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * const directory = yield* fs.makeTempDirectoryScoped() + * const path = `${directory}/message.txt` + * + * yield* fs.writeFileString(path, "hello") + * return yield* fs.readFileString(path) + * }) + * ``` + * + * **Gotchas** + * + * Paths are interpreted by the provided implementation, so relative paths, case + * sensitivity, permissions, links, and watch behavior are platform-dependent. + * Size options are normalized to branded bigint byte counts; prefer the `Size`, + * `KiB`, `MiB`, and related helpers for offsets, chunk sizes, and truncation + * lengths. A program that uses this service still needs a concrete layer, such + * as `NodeFileSystem.layer`, before it can access the real file system. + * + * @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 + /** + * 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 tags + * @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)) + }, + 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-smol/packages/effect/src/Filter.ts b/.repos/effect-smol/packages/effect/src/Filter.ts new file mode 100644 index 00000000000..693a37793ce --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Filter.ts @@ -0,0 +1,852 @@ +/** + * The `Filter` module provides composable functions for accepting, rejecting, + * narrowing, and transforming values. A `Filter` receives an + * input and returns a `Result`: success means the value passed the filter, while + * failure means the value was filtered out. + * + * **Mental model** + * + * - A filter is a typed predicate that can also transform the successful value + * - Predicate-based filters pass the original input when the predicate returns `true` + * - Refinement-based filters narrow the successful type, for example from `unknown` to `string` + * - Custom filters return `Result.succeed(pass)` or `Result.fail(fail)` directly + * - Filters compose with logical and sequential combinators instead of throwing exceptions + * - `FilterEffect` is the effectful form for filters that need asynchronous work, errors, or services + * + * **Common tasks** + * + * - Build filters: {@link make}, {@link makeEffect}, {@link fromPredicate}, {@link fromPredicateOption} + * - Narrow unknown values: {@link string}, {@link number}, {@link boolean}, {@link bigint}, {@link symbol}, {@link date} + * - Match shapes and variants: {@link instanceOf}, {@link tagged}, {@link reason}, {@link has} + * - Match exact values: {@link equals}, {@link equalsStrict} + * - Combine alternatives: {@link or} + * - Require multiple filters: {@link zip}, {@link zipWith}, {@link andLeft}, {@link andRight} + * - Run filters in sequence: {@link compose}, {@link composePassthrough} + * - Convert results: {@link toPredicate}, {@link toOption}, {@link toResult} + * - Adjust failure values: {@link mapFail} + * + * **Gotchas** + * + * - A failed filter is data in the `Result` failure channel; it is not an exception + * - `compose` preserves intermediate failure values, while {@link composePassthrough} fails with the original input + * - `equalsStrict` uses JavaScript `===`; use {@link equals} for structural equality + * - `fromPredicateOption` fails with the original input when the returned `Option` is `None` + * - Prefer refinement predicates when you want TypeScript to narrow the successful value type + * + * @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 narrowing 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 and you only need the + * passed value, with filtered-out inputs represented as `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-smol/packages/effect/src/Formatter.ts b/.repos/effect-smol/packages/effect/src/Formatter.ts new file mode 100644 index 00000000000..2bfdc5cfd34 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Formatter.ts @@ -0,0 +1,367 @@ +/** + * Utilities for converting arbitrary JavaScript values into human-readable + * strings, with support for circular references, redaction, and common JS + * types that `JSON.stringify` handles poorly. + * + * Mental model: + * - A `Formatter` is a callable `(value: Value) => Format`. + * - {@link format} is the general-purpose pretty-printer: it handles + * primitives, arrays, objects, `BigInt`, `Symbol`, `Date`, `RegExp`, + * `Set`, `Map`, class instances, and circular references. + * - {@link formatJson} is a safe `JSON.stringify` wrapper that silently + * drops circular references and applies redaction. + * - Both functions accept a `space` option for indentation control. + * + * Common tasks: + * - Pretty-print any value for debugging / logging -> {@link format} + * - Serialize to JSON safely (no circular throws) -> {@link formatJson} + * - Format a single object property key -> {@link formatPropertyKey} + * - Format a property path like `["a"]["b"]` -> {@link formatPath} + * - Format a `Date` to ISO string safely -> {@link formatDate} + * + * Gotchas: + * - {@link format} output is **not** valid JSON; use {@link formatJson} when + * you need parseable JSON. + * - {@link format} calls `toString()` on objects by default; pass + * `ignoreToString: true` to disable. + * - {@link formatJson} silently omits circular references (the key is + * dropped from the output). + * - Values implementing the `Redactable` protocol are automatically + * redacted by both {@link format} and {@link formatJson}. + * + * **Example** (Pretty-print a value) + * + * ```ts + * import { Formatter } from "effect" + * + * const obj = { name: "Alice", scores: [100, 97] } + * console.log(Formatter.format(obj)) + * // {"name":"Alice","scores":[100,97]} + * + * console.log(Formatter.format(obj, { space: 2 })) + * // { + * // "name": "Alice", + * // "scores": [ + * // 100, + * // 97 + * // ] + * // } + * ``` + * + * See also: {@link Formatter}, {@link format}, {@link formatJson} + * + * @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** (Define 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 to pretty-print values for debugging, logging, or error messages. + * - You need to handle `BigInt`, `Symbol`, `Set`, `Map`, `Date`, `RegExp`, + * or class instances that `JSON.stringify` cannot represent. + * - You want circular references shown as `"[Circular]"` instead of + * throwing. + * + * **Details** + * + * - Output is **not** valid JSON; use {@link formatJson} when you need + * parseable JSON. + * - 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. + * - Circular references are replaced with `"[Circular]"`. + * - `space` — indentation unit (number of spaces, or a string like + * `"\t"`). Defaults to `0` (compact). + * - `ignoreToString` — skip calling `toString()`. Defaults to `false`. + * + * **Example** (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** (Circular reference handling) + * + * ```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 {@link format}). + * - The input may contain circular references and you want them 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` (omitted from + * output). + * - `Redactable` values are automatically redacted before serialization. + * - Types not supported by JSON (`BigInt`, `Symbol`, `undefined`, + * functions) follow standard `JSON.stringify` behavior (omitted or + * `null` in arrays). + * - `space` — indentation unit (number of spaces, or a string like + * `"\t"`). Defaults to `0` (compact). + * + * **Example** (Compact JSON) + * + * ```ts + * import { Formatter } from "effect" + * + * console.log(Formatter.formatJson({ name: "Alice", age: 30 })) + * // {"name":"Alice","age":30} + * ``` + * + * **Example** (Circular reference handling) + * + * ```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-smol/packages/effect/src/Function.ts b/.repos/effect-smol/packages/effect/src/Function.ts new file mode 100644 index 00000000000..e92532e38de --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Function.ts @@ -0,0 +1,1454 @@ +/** + * The `Function` module provides small, pure helpers for defining, composing, + * adapting, and reusing TypeScript functions. It is the foundation for the + * data-first and data-last APIs used throughout Effect, and it includes the + * core pipeline utilities that make those APIs ergonomic. + * + * **Mental model** + * + * - {@link pipe} starts with a value and passes it through one unary function at + * a time + * - {@link flow} composes unary functions into a reusable function + * - {@link dual} builds APIs that support both direct calls and `pipe`-friendly + * data-last calls + * - {@link identity}, {@link constant}, and the `const*` helpers model common + * identity and thunk patterns without allocating ad hoc callbacks + * - {@link tupled}, {@link untupled}, {@link flip}, and {@link apply} adapt + * call shapes without changing the underlying behavior + * - Type helpers such as {@link LazyArg}, {@link FunctionN}, {@link satisfies}, + * and {@link cast} describe or constrain functions at the type level + * + * **Common tasks** + * + * - Build readable transformation pipelines: {@link pipe} + * - Create reusable composed functions: {@link flow}, {@link compose} + * - Define functions callable in both data-first and data-last style: {@link dual} + * - Return a value unchanged: {@link identity} + * - Create thunks and common constant functions: {@link constant}, + * {@link constTrue}, {@link constFalse}, {@link constNull}, + * {@link constUndefined}, {@link constVoid} + * - Convert between rest-argument and tuple-argument functions: {@link tupled}, + * {@link untupled} + * - Express impossible branches: {@link absurd} + * - Cache results for object keys: {@link memoize} + * + * **Gotchas** + * + * - Functions passed to {@link pipe} and {@link flow} are applied left-to-right + * and should be unary at each step + * - {@link dual} uses either an arity or a predicate to decide whether a call is + * data-first or data-last; use a predicate when optional arguments make arity + * ambiguous + * - {@link cast} changes only the static TypeScript type and performs no runtime + * validation + * - {@link memoize} is intended for object keys and stores cached values in a + * `WeakMap` + * + * @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 to represent unary function types in higher-kinded type operations. + * + * **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** (Using arity to determine data-first or data-last style) + * + * ```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** (Using call signatures to define the overloads) + * + * ```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** (Using a predicate to determine data-first or data-last style) + * + * ```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 type utils + * @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 type utils + * @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 an API expects a thunk or callback and every invocation + * should return the same value. + * + * **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 an API expects a thunk and every invocation should return `true`. + * + * **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 an API expects a thunk and every invocation should return `false`. + * + * **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 an API expects a thunk and every invocation should return `null`. + * + * **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 an API expects a thunk and every invocation should return + * `undefined`. + * + * **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 an API expects a thunk used only for its call effect and not for a + * 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 exhaustive checks prove a branch cannot be reached, but + * TypeScript still needs a return value. + * + * **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 utils + * @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 use `pipe` with data-last functions to build readable transformation + * pipelines and to write method-style chains as ordinary function calls. + * + * **Details** + * + * `pipe` 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** (Using pipeline syntax) + * + * 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 utils + * @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 utils + * @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-smol/packages/effect/src/Graph.ts b/.repos/effect-smol/packages/effect/src/Graph.ts new file mode 100644 index 00000000000..75f56c03212 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Graph.ts @@ -0,0 +1,4745 @@ +/** + * The `Graph` module provides immutable and scoped-mutable graph data + * structures for modeling relationships between indexed nodes and edges. A + * graph can be directed or undirected, stores user-defined data on both nodes + * and edges, and exposes traversal, analysis, path finding, transformation, and + * diagram export utilities. + * + * **Mental model** + * + * - Nodes and edges are addressed by stable numeric indices: {@link NodeIndex} + * and {@link EdgeIndex} + * - Node data has type `N`; edge data has type `E` + * - {@link Graph} values are immutable snapshots; use {@link MutableGraph} + * through {@link mutate}, {@link beginMutation}, or constructor callbacks to + * add, remove, or update nodes and edges + * - Directed graphs follow edge direction for neighbors and traversals, while + * undirected graphs treat each edge as connecting both endpoints + * - Missing lookups return `Option`, while structurally invalid operations such + * as adding an edge to a missing node throw {@link GraphError} + * + * **Common tasks** + * + * - Create graphs: {@link directed}, {@link undirected} + * - Mutate safely: {@link mutate}, {@link addNode}, {@link addEdge}, + * {@link removeNode}, {@link removeEdge} + * - Query contents: {@link getNode}, {@link getEdge}, {@link hasNode}, + * {@link hasEdge}, {@link nodeCount}, {@link edgeCount}, {@link neighbors} + * - Transform data: {@link updateNode}, {@link updateEdge}, {@link mapNodes}, + * {@link mapEdges}, {@link filterNodes}, {@link filterEdges}, + * {@link filterMapNodes}, {@link filterMapEdges} + * - Traverse lazily: {@link dfs}, {@link bfs}, {@link topo}, + * {@link dfsPostOrder}, {@link nodes}, {@link edges}, {@link Walker} + * - Analyze structure: {@link isAcyclic}, {@link isBipartite}, + * {@link connectedComponents}, {@link stronglyConnectedComponents}, + * {@link externals} + * - Find paths: {@link dijkstra}, {@link astar}, {@link bellmanFord}, + * {@link floydWarshall} + * - Export diagrams: {@link toGraphViz}, {@link toMermaid} + * + * **Gotchas** + * + * - Only mutable graphs can be changed. Create one with {@link mutate} or by + * passing a callback to {@link directed} / {@link undirected}. + * - Traversal APIs return lazy {@link Walker} values. Use {@link indices}, + * {@link values}, or {@link entries} to choose what each iteration yields. + * - `NodeIndex` and `EdgeIndex` values are identifiers, not array offsets. They + * are not reused after removals. + * - Shortest-path algorithms require a cost function. {@link dijkstra} and + * {@link astar} reject negative weights; use {@link bellmanFord} or + * {@link floydWarshall} when negative weights are part of the model. + * + * @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 Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type { 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 returned by `getEdge` and `edges` when you need + * the 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 structural interface shared by immutable and mutable graphs. + * + * **Details** + * + * Contains the node and edge maps, adjacency indexes, allocation counters, and + * shared protocols used by both `Graph` and `MutableGraph`. + * + * @category models + * @since 3.18.0 + */ +export interface Proto extends Iterable, Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: typeof TypeId + readonly nodes: Map + readonly edges: Map> + readonly adjacency: Map> + readonly reverseAdjacency: Map> + nextNodeIndex: NodeIndex + nextEdgeIndex: EdgeIndex + acyclic: Option.Option +} + +/** + * 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 +} + +/** + * 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 Proto { + readonly type: T + readonly mutable: true +} + +/** + * 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 ProtoGraph = { + [TypeId]: TypeId, + [Symbol.iterator](this: Graph) { + return this.nodes[Symbol.iterator]() + }, + [NodeInspectSymbol](this: Graph) { + return this.toJSON() + }, + [Equal.symbol](this: Graph, that: Equal.Equal): boolean { + if (isGraph(that)) { + if ( + this.nodes.size !== that.nodes.size || + this.edges.size !== that.edges.size || + this.type !== that.type + ) { + return false + } + // Compare nodes + for (const [nodeIndex, nodeData] of this.nodes) { + if (!that.nodes.has(nodeIndex)) { + return false + } + const otherNodeData = that.nodes.get(nodeIndex)! + if (!Equal.equals(nodeData, otherNodeData)) { + return false + } + } + // Compare edges + for (const [edgeIndex, edgeData] of this.edges) { + if (!that.edges.has(edgeIndex)) { + return false + } + const otherEdge = that.edges.get(edgeIndex)! + if (!Equal.equals(edgeData, otherEdge)) { + return false + } + } + return true + } + return false + }, + [Hash.symbol](this: Graph): number { + let hash = Hash.string("Graph") + hash = hash ^ Hash.string(this.type) + hash = hash ^ Hash.number(this.nodes.size) + hash = hash ^ Hash.number(this.edges.size) + for (const [nodeIndex, nodeData] of this.nodes) { + hash = hash ^ (Hash.hash(nodeIndex) + Hash.hash(nodeData)) + } + for (const [edgeIndex, edgeData] of this.edges) { + hash = hash ^ (Hash.hash(edgeIndex) + Hash.hash(edgeData)) + } + return hash + }, + toJSON(this: Graph) { + return { + _id: "Graph", + nodeCount: this.nodes.size, + edgeCount: this.edges.size, + type: this.type + } + }, + toString(this: Graph) { + return `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` }) + +// ============================================================================= +// Constructors +// ============================================================================= + +/** + * Returns `true` if a value has the graph runtime type identifier, narrowing + * it to a `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. + * + * @category guards + * @since 4.0.0 + */ +export const isGraph = (u: unknown): u is Graph => hasProperty(u, TypeId) + +/** + * 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) => void): DirectedGraph => { + const graph: Mutable> = Object.create(ProtoGraph) + graph.type = "directed" + graph.nodes = new Map() + graph.edges = new Map() + graph.adjacency = new Map() + graph.reverseAdjacency = new Map() + graph.nextNodeIndex = 0 + graph.nextEdgeIndex = 0 + graph.acyclic = Option.some(true) + graph.mutable = false + + if (mutate) { + const mutable = beginMutation(graph as DirectedGraph) + mutate(mutable as MutableDirectedGraph) + return endMutation(mutable) + } + + return graph +} + +/** + * Creates an undirected graph, optionally with initial mutations. + * + * **Example** (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) => void): UndirectedGraph => { + const graph: Mutable> = Object.create(ProtoGraph) + graph.type = "undirected" + graph.nodes = new Map() + graph.edges = new Map() + graph.adjacency = new Map() + graph.reverseAdjacency = new Map() + graph.nextNodeIndex = 0 + graph.nextEdgeIndex = 0 + graph.acyclic = Option.some(true) + graph.mutable = false + + if (mutate) { + const mutable = beginMutation(graph) + mutate(mutable as MutableUndirectedGraph) + return endMutation(mutable) + } + + return graph +} + +// ============================================================================= +// Scoped Mutable API +// ============================================================================= + +/** + * Creates a mutable scope for safe graph mutations by copying the data structure. + * + * **Example** (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 => { + // Copy adjacency maps with deep cloned arrays + const adjacency = new Map>() + const reverseAdjacency = new Map>() + + for (const [nodeIndex, edges] of graph.adjacency) { + adjacency.set(nodeIndex, [...edges]) + } + + for (const [nodeIndex, edges] of graph.reverseAdjacency) { + reverseAdjacency.set(nodeIndex, [...edges]) + } + + const mutable: Mutable> = Object.create(ProtoGraph) + mutable.type = graph.type + mutable.nodes = new Map(graph.nodes) + mutable.edges = new Map(graph.edges) + mutable.adjacency = adjacency + mutable.reverseAdjacency = reverseAdjacency + mutable.nextNodeIndex = graph.nextNodeIndex + mutable.nextEdgeIndex = graph.nextEdgeIndex + mutable.acyclic = graph.acyclic + mutable.mutable = true + + return mutable +} + +/** + * Converts a mutable graph back to an immutable graph, ending the mutation scope. + * + * **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 => { + const graph: Mutable> = Object.create(ProtoGraph) + graph.type = mutable.type + graph.nodes = new Map(mutable.nodes) + graph.edges = new Map(mutable.edges) + graph.adjacency = mutable.adjacency + graph.reverseAdjacency = mutable.reverseAdjacency + graph.nextNodeIndex = mutable.nextNodeIndex + graph.nextEdgeIndex = mutable.nextEdgeIndex + graph.acyclic = mutable.acyclic + graph.mutable = false + + 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) => void + ): (graph: Graph) => Graph + ( + graph: Graph, + f: (mutable: MutableGraph) => void + ): Graph +} = dual(2, ( + graph: Graph, + f: (mutable: MutableGraph) => void +): Graph => { + const mutable = beginMutation(graph) + f(mutable) + return endMutation(mutable) +}) + +// ============================================================================= +// Basic Node Operations +// ============================================================================= + +/** + * Adds a new node to a mutable graph and returns its index. + * + * **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 => { + const nodeIndex = mutable.nextNodeIndex + + // Add node data + mutable.nodes.set(nodeIndex, data) + + // Initialize empty adjacency lists + mutable.adjacency.set(nodeIndex, []) + mutable.reverseAdjacency.set(nodeIndex, []) + + // Update graph allocators + mutable.nextNodeIndex = mutable.nextNodeIndex + 1 + + return nodeIndex +} + +/** + * Gets the data associated with a node index 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 => graph.nodes.has(nodeIndex) ? Option.some(graph.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 => 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 => 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 => { + for (const [index, data] of graph.nodes) { + if (predicate(data)) { + return Option.some(index) + } + } + return Option.none() +}) + +/** + * Finds all nodes that match the given predicate. + * + * **Example** (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 results: Array = [] + for (const [index, data] of graph.nodes) { + if (predicate(data)) { + results.push(index) + } + } + return results +}) + +/** + * Finds the first edge that matches the given predicate. + * + * **Example** (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 => { + for (const [edgeIndex, edgeData] of graph.edges) { + if (predicate(edgeData.data, edgeData.source, edgeData.target)) { + return Option.some(edgeIndex) + } + } + return Option.none() +}) + +/** + * Finds all edges that match the given predicate. + * + * **Example** (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 results: Array = [] + for (const [edgeIndex, edgeData] of graph.edges) { + if (predicate(edgeData.data, edgeData.source, edgeData.target)) { + results.push(edgeIndex) + } + } + return results +}) + +/** + * Updates a single node's data by applying a transformation function. + * + * **Example** (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 => { + if (!mutable.nodes.has(index)) { + return + } + + const currentData = mutable.nodes.get(index)! + const newData = f(currentData) + mutable.nodes.set(index, newData) +} + +/** + * Updates a single edge's data by applying a transformation function. + * + * **Example** (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 => { + if (!mutable.edges.has(edgeIndex)) { + return + } + + const currentEdge = mutable.edges.get(edgeIndex)! + const newData = f(currentEdge.data) + mutable.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 => { + // Transform existing node data in place + for (const [index, data] of mutable.nodes) { + const newData = f(data) + mutable.nodes.set(index, newData) + } +} + +/** + * Transforms all edge data in a mutable graph using the provided mapping function. + * + * **Example** (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 => { + // Transform existing edge data in place + for (const [index, edgeData] of mutable.edges) { + const newData = f(edgeData.data) + mutable.edges.set(index, { + ...edgeData, + data: newData + }) + } +} + +/** + * 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 => { + // Reverse all edges by swapping source and target + for (const [index, edgeData] of mutable.edges) { + mutable.edges.set( + index, + new Edge({ + source: edgeData.target, + target: edgeData.source, + data: edgeData.data + }) + ) + } + + // Clear and rebuild adjacency lists with reversed directions + mutable.adjacency.clear() + mutable.reverseAdjacency.clear() + + // Rebuild adjacency lists with reversed directions + for (const [edgeIndex, edgeData] of mutable.edges) { + // Add to forward adjacency (source -> target) + const sourceEdges = mutable.adjacency.get(edgeData.source) || [] + sourceEdges.push(edgeIndex) + mutable.adjacency.set(edgeData.source, sourceEdges) + + // Add to reverse adjacency (target <- source) + const targetEdges = mutable.reverseAdjacency.get(edgeData.target) || [] + targetEdges.push(edgeIndex) + mutable.reverseAdjacency.set(edgeData.target, targetEdges) + } + + // Invalidate cycle flag since edge directions changed + mutable.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 => { + const nodesToRemove: Array = [] + + // First pass: identify nodes to remove and transform data for nodes to keep + for (const [index, data] of mutable.nodes) { + const result = f(data) + if (Option.isSome(result)) { + // Transform node data + mutable.nodes.set(index, result.value) + } else { + // Mark for removal + nodesToRemove.push(index) + } + } + + // Second pass: remove filtered out nodes and their edges + for (const nodeIndex of nodesToRemove) { + removeNode(mutable, nodeIndex) + } +} + +/** + * Filters and optionally transforms edges in a mutable graph using a predicate function. + * Edges that return Option.none are removed from the graph. + * + * **Example** (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 => { + const edgesToRemove: Array = [] + + // First pass: identify edges to remove and transform data for edges to keep + for (const [index, edgeData] of mutable.edges) { + const result = f(edgeData.data) + if (Option.isSome(result)) { + // Transform edge data + mutable.edges.set(index, { + ...edgeData, + data: result.value + }) + } else { + // Mark for removal + edgesToRemove.push(index) + } + } + + // Second pass: remove filtered out edges + for (const edgeIndex of edgesToRemove) { + removeEdge(mutable, edgeIndex) + } +} + +/** + * Filters nodes by removing those that don't match the predicate. + * This function modifies the mutable graph in place. + * + * **Example** (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 => { + const nodesToRemove: Array = [] + + // Identify nodes to remove + for (const [index, data] of mutable.nodes) { + if (!predicate(data)) { + nodesToRemove.push(index) + } + } + + // Remove filtered out nodes (this also removes connected edges) + for (const nodeIndex of nodesToRemove) { + removeNode(mutable, nodeIndex) + } +} + +/** + * Filters edges by removing those that don't match the predicate. + * This function modifies the mutable graph in place. + * + * **Example** (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 => { + const edgesToRemove: Array = [] + + // Identify edges to remove + for (const [index, edgeData] of mutable.edges) { + if (!predicate(edgeData.data)) { + edgesToRemove.push(index) + } + } + + // Remove filtered out edges + for (const edgeIndex of edgesToRemove) { + removeEdge(mutable, edgeIndex) + } +} + +// ============================================================================= +// Cycle Flag Management (Internal) +// ============================================================================= + +/** @internal */ +const invalidateCycleFlagOnRemoval = ( + mutable: MutableGraph +): void => { + // Only invalidate if the graph had cycles (removing edges/nodes cannot introduce cycles in acyclic graphs). + if (mutable.acyclic._tag === "Some" && mutable.acyclic.value === false) { + mutable.acyclic = Option.none() + } +} + +/** @internal */ +const invalidateCycleFlagOnAddition = ( + mutable: MutableGraph +): 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 => { + // Validate that both nodes exist + if (!mutable.nodes.has(source)) { + throw missingNode(source) + } + if (!mutable.nodes.has(target)) { + throw missingNode(target) + } + + const edgeIndex = mutable.nextEdgeIndex + + // Create edge data + const edgeData = new Edge({ source, target, data }) + mutable.edges.set(edgeIndex, edgeData) + + // Update adjacency lists + const sourceAdjacency = mutable.adjacency.get(source) + if (sourceAdjacency !== undefined) { + sourceAdjacency.push(edgeIndex) + } + + const targetReverseAdjacency = mutable.reverseAdjacency.get(target) + if (targetReverseAdjacency !== undefined) { + targetReverseAdjacency.push(edgeIndex) + } + + // For undirected graphs, add reverse connections + if (mutable.type === "undirected") { + const targetAdjacency = mutable.adjacency.get(target) + if (targetAdjacency !== undefined) { + targetAdjacency.push(edgeIndex) + } + + const sourceReverseAdjacency = mutable.reverseAdjacency.get(source) + if (sourceReverseAdjacency !== undefined) { + sourceReverseAdjacency.push(edgeIndex) + } + } + + // Update allocators + mutable.nextEdgeIndex = mutable.nextEdgeIndex + 1 + + // Only invalidate cycle flag if the graph was acyclic + // Adding edges cannot remove cycles from cyclic graphs + invalidateCycleFlagOnAddition(mutable) + + return edgeIndex +} + +/** + * Removes a node and all its incident edges from a mutable graph. + * + * **Example** (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 => { + // Check if node exists + if (!mutable.nodes.has(nodeIndex)) { + return // Node doesn't exist, nothing to remove + } + + // Collect all incident edges for removal + const edgesToRemove: Array = [] + + // Get outgoing edges + const outgoingEdges = mutable.adjacency.get(nodeIndex) + if (outgoingEdges !== undefined) { + for (const edge of outgoingEdges) { + edgesToRemove.push(edge) + } + } + + // Get incoming edges + const incomingEdges = mutable.reverseAdjacency.get(nodeIndex) + if (incomingEdges !== undefined) { + for (const edge of incomingEdges) { + edgesToRemove.push(edge) + } + } + + // Remove all incident edges + for (const edgeIndex of edgesToRemove) { + removeEdgeInternal(mutable, edgeIndex) + } + + // Remove the node itself + mutable.nodes.delete(nodeIndex) + mutable.adjacency.delete(nodeIndex) + mutable.reverseAdjacency.delete(nodeIndex) + + // Only invalidate cycle flag if the graph wasn't already known to be acyclic + // Removing nodes cannot introduce cycles in an acyclic graph + invalidateCycleFlagOnRemoval(mutable) +} + +/** + * Removes an edge from a mutable graph. + * + * **Example** (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 => { + const wasRemoved = removeEdgeInternal(mutable, edgeIndex) + + // Only invalidate cycle flag if an edge was actually removed + // and only if the graph wasn't already known to be acyclic + if (wasRemoved) { + invalidateCycleFlagOnRemoval(mutable) + } +} + +/** @internal */ +const removeEdgeInternal = ( + mutable: MutableGraph, + edgeIndex: EdgeIndex +): boolean => { + // Get edge data + const edge = mutable.edges.get(edgeIndex) + if (edge === undefined) { + return false // Edge doesn't exist, no mutation occurred + } + + const { source, target } = edge + + // Remove from adjacency lists + const sourceAdjacency = mutable.adjacency.get(source) + if (sourceAdjacency !== undefined) { + const index = sourceAdjacency.indexOf(edgeIndex) + if (index !== -1) { + sourceAdjacency.splice(index, 1) + } + } + + const targetReverseAdjacency = mutable.reverseAdjacency.get(target) + if (targetReverseAdjacency !== undefined) { + const index = targetReverseAdjacency.indexOf(edgeIndex) + if (index !== -1) { + targetReverseAdjacency.splice(index, 1) + } + } + + // For undirected graphs, remove reverse connections + if (mutable.type === "undirected") { + const targetAdjacency = mutable.adjacency.get(target) + if (targetAdjacency !== undefined) { + const index = targetAdjacency.indexOf(edgeIndex) + if (index !== -1) { + targetAdjacency.splice(index, 1) + } + } + + const sourceReverseAdjacency = mutable.reverseAdjacency.get(source) + if (sourceReverseAdjacency !== undefined) { + const index = sourceReverseAdjacency.indexOf(edgeIndex) + if (index !== -1) { + sourceReverseAdjacency.splice(index, 1) + } + } + } + + // Remove edge data + mutable.edges.delete(edgeIndex) + + return true // Edge was successfully removed +} + +// ============================================================================= +// Edge Query Operations +// ============================================================================= + +/** + * Gets the edge data associated with an edge index 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(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 adjacencyList = graph.adjacency.get(source) + if (adjacencyList === undefined) { + return false + } + + // Check if any edge in the adjacency list connects to the target + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined && edge.target === target) { + return true + } + } + + return false +}) + +/** + * Returns the number of edges in the graph. + * + * **Example** (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 => graph.edges.size + +/** + * 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) + } + + const adjacencyList = graph.adjacency.get(nodeIndex) + if (adjacencyList === undefined) { + return [] + } + + const result: Array = [] + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + result.push(edge.target) + } + } + + return result +}) + +/** + * Gets neighbors of a node in a specific direction for bidirectional traversal. + * + * **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") + * ``` + * + * @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 => { + const adjacencyMap = direction === "incoming" + ? graph.reverseAdjacency + : graph.adjacency + + const adjacencyList = adjacencyMap.get(nodeIndex) + if (adjacencyList === undefined) { + return [] + } + + const result: Array = [] + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + // For incoming direction, we want the source node instead of target + const neighborNode = direction === "incoming" + ? edge.source + : edge.target + result.push(neighborNode) + } + } + + return result +}) + +// ============================================================================= +// GraphViz Export +// ============================================================================= + +/** + * Configuration options for GraphViz DOT format generation from graphs. + * + * **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 models + * @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 +} + +/** + * 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 utils + * @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 { + edgeLabel = (data: E) => String(data), + graphName = "G", + nodeLabel = (data: N) => String(data) + } = options ?? {} + + const isDirected = graph.type === "directed" + const graphType = isDirected ? "digraph" : "graph" + const edgeOperator = isDirected ? "->" : "--" + + const lines: Array = [] + lines.push(`${graphType} ${graphName} {`) + + // Add nodes + for (const [nodeIndex, nodeData] of graph.nodes) { + const label = nodeLabel(nodeData).replace(/"/g, "\\\"") + lines.push(` "${nodeIndex}" [label="${label}"];`) + } + + // Add edges + for (const [, edgeData] of graph.edges) { + const label = edgeLabel(edgeData.data).replace(/"/g, "\\\"") + lines.push(` "${edgeData.source}" ${edgeOperator} "${edgeData.target}" [label="${label}"];`) + } + + lines.push("}") + return lines.join("\n") +}) + +// ============================================================================= +// Mermaid Export +// ============================================================================= + +/** + * Mermaid node shape types 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 models + * @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" + * }) + * const react = Graph.addNode(mutable, { + * name: "React", + * version: "18.0.0", + * type: "framework" + * }) + * const lodash = Graph.addNode(mutable, { + * name: "Lodash", + * version: "4.17.0", + * type: "library" + * }) + * const webpack = Graph.addNode(mutable, { + * name: "Webpack", + * version: "5.0.0", + * type: "tool" + * }) + * + * 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 utils + * @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 => { + // Extract and validate options with defaults + const { + diagramType, + direction = "TD", + edgeLabel = (data: E) => String(data), + nodeLabel = (data: N) => String(data), + nodeShape = () => "rectangle" as const + } = options ?? {} + + // Auto-detect diagram type if not specified + const finalDiagramType = diagramType ?? + (graph.type === "directed" ? "flowchart" : "graph") + + // Generate diagram header + const lines: Array = [] + lines.push(`${finalDiagramType} ${direction}`) + + // Add nodes + for (const [nodeIndex, nodeData] of graph.nodes) { + const nodeId = String(nodeIndex) + const label = escapeMermaidLabel(nodeLabel(nodeData)) + const shape = nodeShape(nodeData) + const formattedNode = formatMermaidNode(nodeId, label, shape) + lines.push(` ${formattedNode}`) + } + + // Add edges + const edgeOperator = finalDiagramType === "flowchart" ? "-->" : "---" + for (const [, edgeData] of graph.edges) { + const sourceId = String(edgeData.source) + const targetId = String(edgeData.target) + const label = escapeMermaidLabel(edgeLabel(edgeData.data)) + + if (label) { + lines.push(` ${sourceId} ${edgeOperator}|"${label}"| ${targetId}`) + } else { + lines.push(` ${sourceId} ${edgeOperator} ${targetId}`) + } + } + + return lines.join("\n") +}) + +// ============================================================================= +// Direction Types for Bidirectional Traversal +// ============================================================================= + +/** + * Direction for graph traversal, indicating which edges to follow. + * + * **Example** (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") + * Graph.addEdge(mutable, a, b, "A->B") + * }) + * + * // Follow outgoing edges (normal direction) + * const outgoingNodes = Array.from( + * Graph.indices(Graph.dfs(graph, { start: [0], direction: "outgoing" })) + * ) + * + * // Follow incoming edges (reverse direction) + * const incomingNodes = Array.from( + * Graph.indices(Graph.dfs(graph, { start: [1], direction: "incoming" })) + * ) + * ``` + * + * @category models + * @since 3.18.0 + */ +export type Direction = "outgoing" | "incoming" + +// ============================================================================= +// 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 go to the immediate parent 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 => { + // Use existing cycle flag if available + if (Option.isSome(graph.acyclic)) { + return graph.acyclic.value + } + + // Stack-safe DFS cycle detection using iterative approach + const visited = new Set() + const recursionStack = new Set() + + // Stack entry: [node, neighbors, neighborIndex, isFirstVisit] + type DfsStackEntry = [NodeIndex, Array, number, boolean] + + // Get all nodes to handle disconnected components + for (const startNode of graph.nodes.keys()) { + if (visited.has(startNode)) { + continue // Already processed this component + } + + // Iterative DFS with explicit stack + const stack: Array = [[startNode, [], 0, true]] + + while (stack.length > 0) { + const [node, neighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1] + + // First visit to this node + if (isFirstVisit) { + if (recursionStack.has(node)) { + // Back edge found - cycle detected + graph.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 = Array.from(neighborsDirected(graph, node, "outgoing")) + stack[stack.length - 1] = [node, nodeNeighbors, 0, false] + continue + } + + // Process next neighbor + if (neighborIndex < neighbors.length) { + const neighbor = neighbors[neighborIndex] + stack[stack.length - 1] = [node, neighbors, neighborIndex + 1, false] + + if (recursionStack.has(neighbor)) { + // Back edge found - cycle detected + graph.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 + graph.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 coloring = new Map() + const discovered = new Set() + let isBipartiteGraph = true + + // Get all nodes to handle disconnected components + for (const startNode of graph.nodes.keys()) { + if (!discovered.has(startNode)) { + // Start BFS coloring from this component + const queue: Array = [startNode] + coloring.set(startNode, 0) // Color start node with 0 + discovered.add(startNode) + + while (queue.length > 0 && isBipartiteGraph) { + const current = queue.shift()! + const currentColor = coloring.get(current)! + const neighborColor: 0 | 1 = currentColor === 0 ? 1 : 0 + + // Get all neighbors for undirected graph + const nodeNeighbors = getUndirectedNeighbors(graph, current) + for (const neighbor of nodeNeighbors) { + if (!discovered.has(neighbor)) { + // Color unvisited neighbor with opposite color + coloring.set(neighbor, neighborColor) + discovered.add(neighbor) + queue.push(neighbor) + } else { + // Check if neighbor has the same color (conflict) + if (coloring.get(neighbor) === currentColor) { + isBipartiteGraph = false + break + } + } + } + } + + // Early exit if not bipartite + if (!isBipartiteGraph) { + break + } + } + } + + return isBipartiteGraph +} + +/** + * Get neighbors for undirected graphs by checking both adjacency and reverse adjacency. + * For undirected graphs, we need to find the other endpoint of each edge incident to the node. + */ +const getUndirectedNeighbors = ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): Array => { + const neighbors = new Set() + + // Check edges where this node is the source + const adjacencyList = graph.adjacency.get(nodeIndex) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + // For undirected graphs, the neighbor is the other endpoint + const otherNode = edge.source === nodeIndex ? edge.target : edge.source + neighbors.add(otherNode) + } + } + } + + return Array.from(neighbors) +} + +/** + * 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 visited = new Set() + const components: Array> = [] + for (const startNode of graph.nodes.keys()) { + if (!visited.has(startNode)) { + // DFS to find all nodes in this component + const component: Array = [] + const stack: Array = [startNode] + + while (stack.length > 0) { + const current = stack.pop()! + if (!visited.has(current)) { + visited.add(current) + component.push(current) + + // Add all unvisited neighbors to stack + const nodeNeighbors = getUndirectedNeighbors(graph, current) + for (const neighbor of nodeNeighbors) { + if (!visited.has(neighbor)) { + stack.push(neighbor) + } + } + } + } + + components.push(component) + } + } + + return components +} + +/** + * Finds strongly connected components in a directed graph using Kosaraju's algorithm. + * Each SCC is represented as an array of node indices. + * + * **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> => { + const visited = new Set() + const finishOrder: Array = [] + // Iterate directly over node keys + + // Step 1: Stack-safe DFS on original graph to get finish times + // Stack entry: [node, neighbors, neighborIndex, isFirstVisit] + type DfsStackEntry = [NodeIndex, Array, number, boolean] + + for (const startNode of graph.nodes.keys()) { + if (visited.has(startNode)) { + continue + } + + const stack: Array = [[startNode, [], 0, true]] + + while (stack.length > 0) { + const [node, nodeNeighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1] + + if (isFirstVisit) { + if (visited.has(node)) { + stack.pop() + continue + } + + visited.add(node) + const nodeNeighborsList = neighbors(graph, node) + stack[stack.length - 1] = [node, nodeNeighborsList, 0, false] + continue + } + + // Process next neighbor + if (neighborIndex < nodeNeighbors.length) { + const neighbor = nodeNeighbors[neighborIndex] + stack[stack.length - 1] = [node, nodeNeighbors, neighborIndex + 1, false] + + if (!visited.has(neighbor)) { + stack.push([neighbor, [], 0, true]) + } + } else { + // Done with this node - add to finish order (post-order) + finishOrder.push(node) + stack.pop() + } + } + } + + // Step 2: Stack-safe DFS on transpose graph in reverse finish order + visited.clear() + const sccs: Array> = [] + + for (let i = finishOrder.length - 1; i >= 0; i--) { + const startNode = finishOrder[i] + if (visited.has(startNode)) { + continue + } + + const scc: Array = [] + const stack: Array = [startNode] + + while (stack.length > 0) { + const node = stack.pop()! + + if (visited.has(node)) { + continue + } + + visited.add(node) + scc.push(node) + + // Use reverse adjacency (transpose graph) + const reverseAdjacency = graph.reverseAdjacency.get(node) + if (reverseAdjacency !== undefined) { + for (const edgeIndex of reverseAdjacency) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + const predecessor = edge.source + if (!visited.has(predecessor)) { + stack.push(predecessor) + } + } + } + } + } + + sccs.push(scc) + } + + return sccs +} + +// ============================================================================= +// Path Finding Algorithms +// ============================================================================= + +/** + * 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 +} + +/** + * 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. + * + * **Gotchas** + * + * `dijkstra` throws a `GraphError` when either endpoint does not exist or when + * the cost function returns a negative weight. + * + * @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. Returns `Option.none()` when the target is + * not reachable, and throws a `GraphError` when either endpoint is missing or a + * negative edge cost is encountered. + * + * **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> => { + // Validate that source and target nodes exist + if (!graph.nodes.has(config.source)) { + throw missingNode(config.source) + } + if (!graph.nodes.has(config.target)) { + throw missingNode(config.target) + } + + // 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 graph.nodes.keys()) { + distances.set(node, node === config.source ? 0 : Infinity) + previous.set(node, null) + } + + // Simple priority queue using array (can be optimized with proper heap) + const priorityQueue: Array<{ node: NodeIndex; distance: number }> = [ + { node: config.source, distance: 0 } + ] + + while (priorityQueue.length > 0) { + // Find minimum distance node (priority queue extract-min) + let minIndex = 0 + for (let i = 1; i < priorityQueue.length; i++) { + if (priorityQueue[i].distance < priorityQueue[minIndex].distance) { + minIndex = i + } + } + + const current = priorityQueue.splice(minIndex, 1)[0] + const currentNode = current.node + + // Skip if already visited (can happen with duplicate entries) + if (visited.has(currentNode)) { + continue + } + + visited.add(currentNode) + + // Early termination if we reached the target + if (currentNode === config.target) { + break + } + + // Get current distance + const currentDistance = distances.get(currentNode)! + + // Examine all outgoing edges + const adjacencyList = graph.adjacency.get(currentNode) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + const neighbor = edge.target + const cost = config.cost(edge.data) + + // Validate non-negative weights + if (cost < 0) { + throw new GraphError({ message: "Dijkstra's algorithm requires non-negative edge weights" }) + } + + 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)) { + priorityQueue.push({ node: neighbor, distance: newDistance }) + } + } + } + } + } + } + + // 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, but + * a `GraphError` is thrown 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 => { + // Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration) + const allNodes = Array.from(graph.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) + next.get(i)!.set(j, null) + edgeMatrix.get(i)!.set(j, null) + } + } + + // Set edge weights + for (const [, edgeData] of graph.edges) { + const weight = cost(edgeData.data) + const i = edgeData.source + const j = edgeData.target + + // Use minimum weight if multiple edges exist + const currentWeight = 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) + } + } + + // 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) { + distances.get(i)!.set(j, distIK + distKJ) + next.get(i)!.set(j, next.get(i)!.get(k)!) + } + } + } + } + + // Check for negative cycles + for (const i of allNodes) { + if (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 === null) break + + const edgeData = edgeMatrix.get(current)!.get(nextNode)! + if (edgeData !== null) { + weights.push(edgeData) + } + + current = nextNode + path.push(current) + } + + paths.get(i)!.set(j, path) + 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, 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 the heuristic + * should be admissible to preserve shortest-path guarantees. Returns + * `Option.none()` when the target is not reachable, and throws a `GraphError` + * when either endpoint is missing or a negative edge cost is encountered. + * + * **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> => { + // Validate that source and target nodes exist + if (!graph.nodes.has(config.source)) { + throw missingNode(config.source) + } + if (!graph.nodes.has(config.target)) { + throw missingNode(config.target) + } + + // 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 graph.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) + } + + // Priority queue using f-score (total estimated cost) + const openSet: Array<{ node: NodeIndex; fScore: number }> = [ + { node: config.source, fScore: fScore.get(config.source)! } + ] + + while (openSet.length > 0) { + // Find node with lowest f-score + let minIndex = 0 + for (let i = 1; i < openSet.length; i++) { + if (openSet[i].fScore < openSet[minIndex].fScore) { + minIndex = i + } + } + + const current = openSet.splice(minIndex, 1)[0] + const currentNode = current.node + + // Skip if already visited + if (visited.has(currentNode)) { + continue + } + + visited.add(currentNode) + + // Early termination if we reached the target + if (currentNode === config.target) { + break + } + + // Get current g-score + const currentGScore = gScore.get(currentNode)! + + // Examine all outgoing edges + const adjacencyList = graph.adjacency.get(currentNode) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + const neighbor = edge.target + const weight = config.cost(edge.data) + + // Validate non-negative weights + if (weight < 0) { + throw new GraphError({ message: "A* algorithm requires non-negative edge weights" }) + } + + 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)) { + openSet.push({ node: neighbor, fScore: f }) + } + } + } + } + } + } + } + + // 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. 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. + * + * **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> => { + // Validate that source and target nodes exist + if (!graph.nodes.has(config.source)) { + throw missingNode(config.source) + } + if (!graph.nodes.has(config.target)) { + throw missingNode(config.target) + } + + // Early return if source equals target + if (config.source === config.target) { + return Option.some({ + path: [config.source], + distance: 0, + costs: [] + }) + } + + // Initialize distances and predecessors + const distances = new Map() + const previous = new Map() + + // Iterate directly over node keys + for (const node of graph.nodes.keys()) { + distances.set(node, node === 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 graph.edges) { + const weight = config.cost(edgeData.data) + edges.push({ + source: edgeData.source, + target: edgeData.target, + weight, + edgeData: edgeData.data + }) + } + + // Relax edges up to V-1 times + const nodeCount = graph.nodes.size + for (let i = 0; i < nodeCount - 1; i++) { + let hasUpdate = false + + for (const edge of edges) { + const sourceDistance = distances.get(edge.source)! + const targetDistance = distances.get(edge.target)! + + // Relaxation step + if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) { + distances.set(edge.target, sourceDistance + edge.weight) + previous.set(edge.target, { node: edge.source, edgeData: edge.edgeData }) + hasUpdate = true + } + } + + // Early termination if no updates + if (!hasUpdate) { + break + } + } + + // Check for negative cycles + for (const edge of edges) { + const sourceDistance = distances.get(edge.source)! + const targetDistance = distances.get(edge.target)! + + if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) { + // Negative cycle detected - check if it affects the path to target + const affectedNodes = new Set() + const queue = [edge.target] + + while (queue.length > 0) { + const node = queue.shift()! + if (affectedNodes.has(node)) continue + affectedNodes.add(node) + + // Add all nodes reachable from this node + const adjacencyList = graph.adjacency.get(node) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + queue.push(edge.target) + } + } + } + } + + // If target is affected by 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 utils + * @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 utils + * @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 utils + * @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 or incoming edges. + * + * **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?: Direction +} + +/** + * 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. 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" + + // Validate that all start nodes exist + for (const nodeIndex of start) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const stack = [...start] + const discovered = new Set() + + const nextMapped = () => { + while (stack.length > 0) { + const current = stack.pop()! + + if (discovered.has(current)) { + continue + } + + discovered.add(current) + + const nodeDataOption = getNode(graph, current) + if (Option.isNone(nodeDataOption)) { + continue + } + + const neighbors = neighborsDirected(graph, current, direction) + for (let i = neighbors.length - 1; i >= 0; i--) { + const neighbor = neighbors[i] + if (!discovered.has(neighbor)) { + stack.push(neighbor) + } + } + + return { done: false, value: f(current, nodeDataOption.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. 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" + + // Validate that all start nodes exist + for (const nodeIndex of start) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const queue = [...start] + const discovered = new Set() + + const nextMapped = () => { + while (queue.length > 0) { + const current = queue.shift()! + + if (!discovered.has(current)) { + discovered.add(current) + + const neighbors = neighborsDirected(graph, current, direction) + for (const neighbor of neighbors) { + if (!discovered.has(neighbor)) { + queue.push(neighbor) + } + } + + const nodeData = getNode(graph, current) + if (Option.isSome(nodeData)) { + return { done: false, value: f(current, nodeData.value) } + } + return nextMapped() + } + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +}) + +/** + * Configuration for the topological sort iterator. + * + * **When to use** + * + * Use to seed a topological sort with specific initial node indices instead of + * starting from every zero in-degree node. + * + * **Details** + * + * `initials` optionally supplies the node indices used as initial queue + * entries. When omitted, topological sorting starts from all nodes with zero + * in-degree. + * + * @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 => { + // 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 inDegree = new Map() + const remaining = new Set() + const queue = [...initials] + + // Initialize in-degree counts + for (const [nodeIndex] of graph.nodes) { + inDegree.set(nodeIndex, 0) + remaining.add(nodeIndex) + } + + // Calculate in-degrees + for (const [, edgeData] of graph.edges) { + const currentInDegree = inDegree.get(edgeData.target) || 0 + inDegree.set(edgeData.target, currentInDegree + 1) + } + + // Add nodes with zero in-degree to queue if no initials provided + if (initials.length === 0) { + for (const [nodeIndex, degree] of inDegree) { + if (degree === 0) { + queue.push(nodeIndex) + } + } + } + + const nextMapped = () => { + while (queue.length > 0) { + const current = queue.shift()! + + if (remaining.has(current)) { + remaining.delete(current) + + // Process outgoing edges, reducing in-degree of targets + const neighbors = neighborsDirected(graph, current, "outgoing") + for (const neighbor of neighbors) { + if (remaining.has(neighbor)) { + const currentInDegree = inDegree.get(neighbor) || 0 + const newInDegree = currentInDegree - 1 + inDegree.set(neighbor, newInDegree) + + // If in-degree becomes 0, add to queue + if (newInDegree === 0) { + queue.push(neighbor) + } + } + } + + const nodeData = getNode(graph, current) + if (Option.isSome(nodeData)) { + return { done: false, value: f(current, nodeData.value) } + } + return nextMapped() + } + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +}) + +/** + * Creates a 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. + * + * **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" + + // Validate that all start nodes exist + for (const nodeIndex of start) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const stack: Array<{ node: NodeIndex; visitedChildren: boolean }> = [] + const discovered = new Set() + const finished = new Set() + + // Initialize stack with start nodes + for (let i = start.length - 1; i >= 0; i--) { + stack.push({ node: start[i], visitedChildren: false }) + } + + const nextMapped = () => { + while (stack.length > 0) { + const current = stack[stack.length - 1] + + if (!discovered.has(current.node)) { + discovered.add(current.node) + current.visitedChildren = false + } + + if (!current.visitedChildren) { + current.visitedChildren = true + const neighbors = neighborsDirected(graph, current.node, direction) + + for (let i = neighbors.length - 1; i >= 0; i--) { + const neighbor = neighbors[i] + if (!discovered.has(neighbor) && !finished.has(neighbor)) { + stack.push({ node: neighbor, visitedChildren: false }) + } + } + } else { + const nodeToEmit = stack.pop()!.node + + if (!finished.has(nodeToEmit)) { + finished.add(nodeToEmit) + + const nodeData = getNode(graph, nodeToEmit) + if (Option.isSome(nodeData)) { + return { done: false, value: f(nodeToEmit, nodeData.value) } + } + return nextMapped() + } + } + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +}) + +/** + * Creates an iterator over all node indices in the graph. + * + * **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 = 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 = 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 nodeMap = graph.nodes + const adjacencyMap = direction === "incoming" + ? graph.reverseAdjacency + : graph.adjacency + + const nodeIterator = nodeMap.entries() + + const nextMapped = () => { + let current = nodeIterator.next() + while (!current.done) { + const [nodeIndex, nodeData] = current.value + const adjacencyList = adjacencyMap.get(nodeIndex) + + // Node is external if it has no edges in the specified direction + if (adjacencyList === undefined || adjacencyList.length === 0) { + return { done: false, value: f(nodeIndex, nodeData) } + } + current = nodeIterator.next() + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +}) diff --git a/.repos/effect-smol/packages/effect/src/HKT.ts b/.repos/effect-smol/packages/effect/src/HKT.ts new file mode 100644 index 00000000000..df79b6ffc13 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/HKT.ts @@ -0,0 +1,255 @@ +/** + * Type-level encoding for higher-kinded types in Effect. + * + * TypeScript cannot abstract directly over type constructors such as + * `Option<_>`, `ReadonlyArray<_>`, or `Effect<_, _, _>`. This module encodes + * those constructors with {@link TypeLambda} and applies them with + * {@link Kind}, so libraries can define generic APIs that work across many + * Effect data types. + * + * **Mental model** + * + * - A {@link TypeLambda} is a type-level function with four slots: `In`, + * `Out2`, `Out1`, and `Target` + * - A concrete type lambda defines `readonly type` in terms of those slots + * - {@link Kind} fills the slots and reads the lambda's resulting concrete type + * - {@link TypeClass} lets an interface carry the lambda it implements through + * {@link URI} + * - Effect modules expose their own type lambdas when they support generic + * higher-kinded programming + * + * **Common tasks** + * + * - Define a type lambda for a data type by extending {@link TypeLambda} + * - Apply a lambda to type arguments with {@link Kind} + * - Write type class interfaces that are parameterized by a lambda + * + * **Gotchas** + * + * - The slot names are positional; check the concrete lambda to see how `In`, + * `Out2`, `Out1`, and `Target` map to that data type's parameters + * - Use `never` for slots that a lambda does not read + * - HKT values are type-level encodings; they do not create runtime wrappers + * + * **Example** (Defining a simple type lambda) + * + * ```ts + * import type { HKT } from "effect" + * + * interface ReadonlyArrayTypeLambda extends HKT.TypeLambda { + * readonly type: ReadonlyArray + * } + * + * type StringArray = HKT.Kind< + * ReadonlyArrayTypeLambda, + * never, + * never, + * never, + * string + * > + * ``` + * + * @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 defining a custom type class that needs to expose 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 type utils + * @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-smol/packages/effect/src/Hash.ts b/.repos/effect-smol/packages/effect/src/Hash.ts new file mode 100644 index 00000000000..b1b216108ff --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Hash.ts @@ -0,0 +1,580 @@ +/** + * The `Hash` module 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. + * + * **Mental model** + * + * - {@link hash} dispatches by JavaScript type and handles primitives, + * arrays, typed arrays, maps, sets, plain objects, dates, regular + * expressions, and custom hashable objects + * - Objects can implement {@link Hash} by defining a method at {@link symbol} + * - Structural object hashes are cached, so repeated hashing of the same object + * is cheap after the first computation + * - {@link random} gives reference-stable hash values for values that should + * be hashed by identity + * - Lower-level helpers such as {@link combine}, {@link string}, + * {@link number}, {@link structure}, {@link structureKeys}, and + * {@link array} are useful when implementing custom hashes + * + * **Quickstart** + * + * **Example** (Implementing a custom hash) + * + * ```ts + * import { Hash } from "effect" + * + * class UserKey implements Hash.Hash { + * constructor( + * readonly id: string, + * readonly region: string + * ) {} + * + * [Hash.symbol](): number { + * return Hash.combine(Hash.string(this.region))(Hash.string(this.id)) + * } + * } + * + * const value = Hash.hash(new UserKey("user-1", "eu")) + * ``` + * + * **Gotchas** + * + * - Hash collisions are possible; hash-based collections also need equality + * semantics to decide whether two values are actually the same + * - Do not mutate an object after hashing it structurally, because the cached + * hash can become stale + * - Use {@link random} or a custom {@link Hash} implementation for mutable + * objects that should be compared by reference identity + * + * @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 to hash a string directly. + * + * **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-smol/packages/effect/src/HashMap.ts b/.repos/effect-smol/packages/effect/src/HashMap.ts new file mode 100644 index 00000000000..bebb6efdb6f --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/HashMap.ts @@ -0,0 +1,1294 @@ +/** + * The `HashMap` module provides an immutable key-value data structure with + * efficient lookup, insertion, removal, and transformation operations. A + * `HashMap` stores entries by hashing keys and resolving matches + * with Effect's structural equality semantics. + * + * **Mental model** + * + * - A `HashMap` is an immutable collection of key-value pairs + * - Keys are compared using the `Equal` protocol and are grouped by hashes from + * the `Hash` protocol + * - Plain JavaScript primitives work as keys, and custom objects can define + * `Equal` / `Hash` behavior for structural lookup + * - Lookups with {@link get} return an `Option`, making missing keys explicit + * - Iteration order is based on the map's internal hash structure and should + * not be treated as insertion order + * + * **Common tasks** + * + * - Create maps: {@link empty}, {@link make}, {@link fromIterable} + * - Read values: {@link get}, {@link getUnsafe}, {@link has}, {@link hasBy} + * - Add or update entries: {@link set}, {@link modify}, {@link modifyAt}, {@link setMany} + * - Remove entries: {@link remove}, {@link removeMany} + * - Combine maps: {@link union} + * - Iterate or convert: {@link keys}, {@link values}, {@link entries}, {@link toValues}, {@link toEntries} + * - Transform values: {@link map}, {@link flatMap}, {@link filter}, {@link filterMap}, {@link compact} + * - Fold and search: {@link reduce}, {@link findFirst}, {@link some}, {@link every} + * - Batch updates efficiently: {@link mutate}, {@link beginMutation}, {@link endMutation} + * + * **Gotchas** + * + * - {@link getUnsafe} throws when the key is absent; prefer {@link get} unless + * absence is impossible by construction + * - Mutating a key object after insertion can make future lookups fail if its + * equality or hash changes + * - Hash collisions are handled by equality checks, so matching hashes alone do + * not make two keys equal + * - Use {@link getHash} and {@link hasHash} only when you already have the + * correct hash for the same key + * - Convert entries to an array and sort them when deterministic presentation is + * required + * + * **Quickstart** + * + * **Example** (Working with immutable maps) + * + * ```ts + * import { HashMap, Option } from "effect" + * + * const scores = HashMap.make(["alice", 10], ["bob", 15]) + * + * const updated = scores.pipe( + * HashMap.set("carol", 20), + * HashMap.modify("alice", (score) => score + 1) + * ) + * + * console.log(HashMap.get(updated, "alice")) + * // Output: Option.some(11) + * + * console.log(HashMap.get(scores, "carol")) + * // Output: Option.none() + * + * console.log(Option.getOrElse(HashMap.get(updated, "dave"), () => 0)) + * // Output: 0 + * ``` + * + * **See also** + * + * - `HashSet` for immutable sets backed by hash semantics + * - {@link Equal} for structural equality + * - `Hash` for hash implementations used by hashed collections + * + * @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 type-level + * @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 type-level + * @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 type-level + * @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. + * + * **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** (FlatMapping 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-smol/packages/effect/src/HashRing.ts b/.repos/effect-smol/packages/effect/src/HashRing.ts new file mode 100644 index 00000000000..e027ff1eaa8 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/HashRing.ts @@ -0,0 +1,476 @@ +/** + * The `HashRing` module provides a weighted consistent-hashing data structure + * for assigning arbitrary string inputs to a changing set of nodes. A hash ring + * minimizes remapping when nodes are added, removed, or reweighted, which makes + * it useful for routing requests, partitioning keys, and distributing shards + * across service instances or storage backends. + * + * **Mental model** + * + * - Each node is identified by its {@link PrimaryKey.PrimaryKey} value + * - {@link add} and {@link addMany} place weighted virtual points on the ring + * - {@link get} hashes an input string and returns the nearest node on the ring + * - {@link getShards} assigns a fixed number of shard indexes across the nodes + * - Higher weights receive proportionally more virtual points and shard + * allocations + * - Operations mutate and return the same ring instance + * + * **Common tasks** + * + * - Create an empty ring: {@link make} + * - Add or update nodes: {@link add}, {@link addMany} + * - Remove nodes: {@link remove} + * - Check membership by primary key: {@link has} + * - Route an input key to a node: {@link get} + * - Precompute shard ownership: {@link getShards} + * - Guard unknown values: {@link isHashRing} + * + * **Gotchas** + * + * - Empty rings return `undefined` from {@link get} and {@link getShards} + * - Nodes with the same primary key represent the same ring member + * - Weights are clamped to a positive minimum so a node remains represented + * - Mutating a ring in place is intentional; create a new ring when independent + * snapshots are required + * + * **Quickstart** + * + * **Example** (Routing keys across nodes) + * + * ```ts + * import { HashRing, PrimaryKey } from "effect" + * + * class Node implements PrimaryKey.PrimaryKey { + * constructor(readonly id: string) {} + * + * [PrimaryKey.symbol](): string { + * return this.id + * } + * } + * + * const ring = HashRing.make().pipe( + * HashRing.add(new Node("node-a")), + * HashRing.add(new Node("node-b"), { weight: 2 }) + * ) + * + * const owner = HashRing.get(ring, "user:123") + * console.log(owner ? PrimaryKey.value(owner) : undefined) + * ``` + * + * @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 to 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 to check whether a node value is already registered in a ring by its + * `PrimaryKey` value. + * + * **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-smol/packages/effect/src/HashSet.ts b/.repos/effect-smol/packages/effect/src/HashSet.ts new file mode 100644 index 00000000000..8a2cd789cce --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/HashSet.ts @@ -0,0 +1,634 @@ +/** + * The `HashSet` module provides an immutable set data structure for storing + * unique values with efficient membership checks, additions, removals, and set + * operations. A `HashSet` contains at most one value for each equality class + * as determined by Effect's `Equal` / `Hash` semantics. + * + * **Mental model** + * + * - `HashSet` is an immutable collection of unique values of type `A` + * - Operations such as {@link add}, {@link remove}, {@link union}, and + * {@link difference} return new sets; the input set is never mutated + * - Membership is checked with {@link has}, using Effect equality and hashing + * rather than array-style linear scanning + * - Duplicate values are collapsed when using {@link make}, {@link fromIterable}, + * {@link add}, or {@link map} + * - `HashSet` is iterable, but iteration order is not a sorting guarantee + * + * **Common tasks** + * + * - Create sets: {@link empty}, {@link make}, {@link fromIterable} + * - Check membership and size: {@link has}, {@link size}, {@link isEmpty} + * - Add or remove values: {@link add}, {@link remove} + * - Combine sets: {@link union}, {@link intersection}, {@link difference} + * - Compare sets: {@link isSubset} + * - Transform or select values: {@link map}, {@link filter} + * - Test values: {@link some}, {@link every} + * - Fold values: {@link reduce} + * + * **Gotchas** + * + * - Values that should compare structurally should implement compatible + * `Equal` and `Hash` behavior; otherwise object identity may affect whether + * values are considered distinct + * - {@link map} may reduce the set size when multiple input values map to the + * same output value + * - Do not rely on iteration order for deterministic presentation; sort the + * values after converting to an array when order matters + * + * @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 type-level + * @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-smol/packages/effect/src/Inspectable.ts b/.repos/effect-smol/packages/effect/src/Inspectable.ts new file mode 100644 index 00000000000..1596b30b036 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Inspectable.ts @@ -0,0 +1,383 @@ +/** + * Inspection protocol for stable string, JSON, and Node.js representations. + * + * This module is the small bridge used by Effect data types to explain + * themselves in logs, REPLs, test failures, and JSON-like diagnostics. Implement + * `Inspectable` or extend {@link Class} when a value should expose one + * representation to `toString`, `toJSON`, and Node's `util.inspect`; use + * {@link toJson} and {@link toStringUnknown} when formatting values supplied by + * user code. + * + * ## Mental model + * + * Inspectable values choose their own JSON representation. {@link BaseProto} + * and {@link Class} derive `toString()` from that representation with the + * formatter and expose the same value through {@link NodeInspectSymbol}. + * {@link toJson} is defensive: it calls zero-argument `toJSON` methods, + * recurses through arrays, returns `"[toJSON threw]"` if a custom serializer + * fails, and applies redaction to other values. + * + * ## Common tasks + * + * - Extend {@link Class} for classes that only need to define `toJSON`. + * - Reuse {@link BaseProto} for object prototypes that should share standard + * inspection behavior. + * - Format unknown diagnostic values with {@link toStringUnknown}. + * - Implement {@link NodeInspectSymbol} when integrating directly with + * Node.js inspection. + * + * ## Gotchas + * + * `toJson` is meant for inspection, not canonical persistence. It catches + * `toJSON` failures, does not deeply traverse arbitrary objects, and may + * replace redactable values according to current redaction behavior. Keep + * custom `toJSON` implementations side-effect free so logging and debugging do + * not change program state. + * + * **Example** (Creating inspectable values) + * + * ```ts + * import { Inspectable } from "effect" + * + * class User extends Inspectable.Class { + * constructor( + * readonly id: number, + * readonly name: string + * ) { + * super() + * } + * + * toJSON() { + * return { + * _tag: "User", + * id: this.id, + * name: this.name, + * } + * } + * } + * + * const user = new User(1, "Alice") + * console.log(user.toString()) + * console.log(user[Inspectable.NodeInspectSymbol]()) + * ``` + * + * @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-smol/packages/effect/src/Iterable.ts b/.repos/effect-smol/packages/effect/src/Iterable.ts new file mode 100644 index 00000000000..b4546d3bf2a --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Iterable.ts @@ -0,0 +1,2518 @@ +/** + * The `Iterable` module works with any JavaScript value that implements + * `[Symbol.iterator]`, including arrays, strings, generators, sets, and custom + * lazy sequences. It provides constructors, transformations, searches, grouping, + * and folding helpers that can be used without converting to an array first. + * + * **Mental model** + * + * - An `Iterable` creates an `Iterator` when it is consumed. + * - Transformations such as {@link map}, {@link filter}, {@link flatMap}, + * {@link take}, and {@link drop} return new iterables and usually do no work + * until the result is iterated. + * - Consumers such as {@link reduce}, {@link size}, {@link head}, + * {@link findFirst}, and `Array.from` pull values from an iterator. + * - Constructors such as {@link range}, {@link makeBy}, {@link repeat}, + * {@link forever}, and {@link unfold} can represent unbounded sequences. + * + * **Common tasks** + * + * - Create sequences: {@link empty}, {@link of}, {@link range}, {@link makeBy}, + * {@link replicate}, {@link unfold} + * - Transform values: {@link map}, {@link flatMap}, {@link flatten}, + * {@link filter}, {@link filterMap} + * - Slice and search: {@link take}, {@link drop}, {@link takeWhile}, + * {@link findFirst}, {@link findLast}, {@link contains} + * - Combine and group: {@link appendAll}, {@link zipWith}, {@link chunksOf}, + * {@link groupBy}, {@link cartesian} + * - Fold or collect: {@link reduce}, {@link scan}, {@link countBy}, + * `Array.from` + * + * **Gotchas** + * + * - Laziness depends on the operation. Functions such as {@link size} and + * {@link reduce} consume the iterable immediately. + * - Some JavaScript iterables are single-use iterators. Reusing the same value + * after it has been consumed may produce no elements. + * - Unbounded iterables must be limited before full collection, for example + * with {@link take}. + * + * **Example** (Building a lazy pipeline) + * + * ```ts + * import { Iterable } from "effect" + * + * const naturals = Iterable.range(1) + * const squares = Iterable.map(naturals, (n) => n * n) + * const evenSquares = Iterable.filter(squares, (n) => n % 2 === 0) + * const firstFive = Iterable.take(evenSquares, 5) + * + * console.log(Array.from(firstFive)) + * // [4, 16, 36, 64, 100] + * ``` + * + * @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 concatenating + * @since 2.0.0 + */ +export const prepend: { + (head: B): (self: Iterable) => Iterable + (self: Iterable, head: B): Iterable +} = dual(2, (self: Iterable, head: B): Iterable => prependAll(self, [head])) + +/** + * Prepends the specified prefix iterable to the beginning of the specified iterable. + * + * **Example** (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 concatenating + * @since 2.0.0 + */ +export const prependAll: { + (that: Iterable): (self: Iterable) => Iterable + (self: Iterable, that: Iterable): Iterable +} = dual( + 2, + (self: Iterable, that: Iterable): Iterable => appendAll(that, self) +) + +/** + * 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 concatenating + * @since 2.0.0 + */ +export const append: { + (last: B): (self: Iterable) => Iterable + (self: Iterable, last: B): Iterable +} = dual(2, (self: Iterable, last: B): Iterable => appendAll(self, [last])) + +/** + * Concatenates two iterables, combining their elements. + * + * **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 concatenating + * @since 2.0.0 + */ +export const appendAll: { + (that: Iterable): (self: Iterable) => Iterable + (self: Iterable, that: Iterable): Iterable +} = dual( + 2, + (self: Iterable, that: Iterable): Iterable => ({ + [Symbol.iterator]() { + const iterA = self[Symbol.iterator]() + let doneA = false + let iterB: Iterator + return { + next() { + if (!doneA) { + const r = iterA.next() + if (r.done) { + doneA = true + iterB = that[Symbol.iterator]() + return iterB.next() + } + return r + } + return iterB.next() + } + } + } + }) +) + +/** + * 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 a `Iterable`, or throw an error 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 concatenating + * @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 as a base case for operations or when you + * need to represent "no data" in a type-safe way. + * + * **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 - 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** (FlatMapping 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** (FlatMapping 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 => + flatMap(self, (a) => map(that, (b) => f(a, b))) +) + +/** + * 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-smol/packages/effect/src/JsonPatch.ts b/.repos/effect-smol/packages/effect/src/JsonPatch.ts new file mode 100644 index 00000000000..75d76110581 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/JsonPatch.ts @@ -0,0 +1,523 @@ +/** + * 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. + * + * **Mental model** + * + * - A {@link JsonPatch} is applied from left to right; each operation observes + * the document produced by the operations before it. + * - Paths use JSON Pointer syntax. The empty path `""` targets the whole + * document, and `/users/0/name` targets a nested property or array element. + * - This module implements the deterministic `add`, `remove`, and `replace` + * subset of RFC 6902. It does not model `test`, `move`, or `copy`. + * - {@link get} compares JSON structure, not domain meaning. It can detect that + * a value changed, but it does not infer semantic edits such as array moves. + * - {@link apply} copies changed containers and returns a new JSON value. An + * empty patch returns the original document reference. + * + * **Common tasks** + * + * - Compute a structural diff between two JSON values with {@link get}. + * - Apply generated or hand-written operations with {@link apply}. + * - Accept, store, or serialize complete patch documents as {@link JsonPatch}. + * - Type individual operations with {@link JsonPatchOperation}. + * + * **Gotchas** + * + * - Generated patches are deterministic, but not guaranteed to be minimal. + * - Array removals are emitted from highest index to lowest so later removals do + * not shift earlier targets. + * - `"-"` is valid only as the final token for array append operations. + * - Invalid paths, missing properties, and out-of-bounds array indices throw. + * - Root `add` and `replace` operations replace the whole document; root + * `remove` is unsupported. + * + * **Example** (Computing and applying a patch) + * + * ```ts + * import { JsonPatch } from "effect" + * + * const before = { title: "draft", tags: ["fp"] } + * const after = { title: "published", tags: ["fp", "effect"] } + * + * const patch = JsonPatch.get(before, after) + * // [ + * // { op: "add", path: "/tags/1", value: "effect" }, + * // { op: "replace", path: "/title", value: "published" } + * // ] + * + * const updated = JsonPatch.apply(patch, before) + * // { title: "published", tags: ["fp", "effect"] } + * ``` + * + * **See also** + * + * - `JsonPointer` for escaping and unescaping JSON Pointer path tokens. + * - {@link Schema.Json} for the JSON value shape accepted by this module. + * + * @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** (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** (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 differences between JSON documents, detect structural + * changes, or create deterministic update operations from before and after + * states. + * + * **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-smol/packages/effect/src/JsonPointer.ts b/.repos/effect-smol/packages/effect/src/JsonPointer.ts new file mode 100644 index 00000000000..12413b7623e --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/JsonPointer.ts @@ -0,0 +1,129 @@ +/** + * Utilities for escaping and unescaping JSON Pointer reference tokens according to RFC 6901. + * + * JSON Pointer (RFC 6901) defines a string syntax for identifying a specific value within a JSON document. + * A JSON Pointer is a sequence of reference tokens separated by forward slashes (`/`). Each reference token + * must be escaped when it contains special characters (`~` or `/`). + * + * ## Mental model + * + * - **Reference token**: A single segment of a JSON Pointer path (e.g., `"foo"`, `"bar/baz"`, `"key~with~tilde"`) + * - **Escaping**: Encoding special characters in a token so it can be safely used in a JSON Pointer (`~` → `~0`, `/` → `~1`) + * - **Unescaping**: Decoding escaped characters back to their original form (`~0` → `~`, `~1` → `/`) + * - **RFC 6901 compliance**: These functions implement the standard escaping rules for JSON Pointer reference tokens + * - **Pure functions**: Both operations are pure, immutable, and have no side effects + * + * ## Common tasks + * + * - Building JSON Pointers from path segments → {@link escapeToken} + * - Parsing JSON Pointers to extract original token values → {@link unescapeToken} + * - Escaping object keys or path segments before constructing JSON Pointers → {@link escapeToken} + * - Extracting unescaped identifiers from JSON Pointer strings → {@link unescapeToken} + * + * ## Gotchas + * + * - These functions operate on **reference tokens**, not full JSON Pointers. A full JSON Pointer like `/foo/bar` must be split into tokens (`["foo", "bar"]`) before escaping/unescaping + * - The order of replacement operations matters: `escapeToken` replaces `~` before `/` to avoid double-escaping + * - Empty strings are valid tokens and are returned unchanged + * - These functions do not validate JSON Pointer syntax; they only handle token-level escaping + * + * ## Quickstart + * + * **Example** (Building and parsing a JSON Pointer) + * + * ```ts + * import { JsonPointer } from "effect" + * + * // Build a JSON Pointer from path segments + * const segments = ["users", "name/alias", "value"] + * const pointer = "/" + segments.map(JsonPointer.escapeToken).join("/") + * // "/users/name~1alias/value" + * + * // Parse a JSON Pointer back to segments + * const tokens = pointer.split("/").slice(1).map(JsonPointer.unescapeToken) + * // ["users", "name/alias", "value"] + * ``` + * + * ## See also + * + * - {@link JsonPatch} - Uses these utilities for JSON Patch operations + * - {@link JsonSchema} - Uses these utilities for schema reference resolution + * + * @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 to build JSON Pointers from object keys or path segments that may contain special characters + * - Escaping tokens before joining them with `/` to form a complete JSON Pointer + * - Preparing reference tokens for use in JSON Patch operations or schema references + * + * **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 to parse JSON Pointers to extract the original token values from escaped segments + * - Converting escaped tokens back to their original form for use as object keys or identifiers + * - Resolving schema references or JSON Patch paths that use escaped tokens + * + * **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-smol/packages/effect/src/JsonSchema.ts b/.repos/effect-smol/packages/effect/src/JsonSchema.ts new file mode 100644 index 00000000000..4a96c3a8161 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/JsonSchema.ts @@ -0,0 +1,1020 @@ +/** + * Convert JSON Schema documents between dialects (Draft-07, Draft-2020-12, + * OpenAPI 3.0, OpenAPI 3.1). All dialects are normalized to an internal + * `Document<"draft-2020-12">` representation before optional conversion to + * an output dialect. + * + * ## Mental model + * + * - **JsonSchema** — a plain object with string keys; represents any single + * JSON Schema node. + * - **Dialect** — one of `"draft-07"`, `"draft-2020-12"`, `"openapi-3.1"`, + * or `"openapi-3.0"`. + * - **Document** — a structured container holding a root `schema`, its + * companion `definitions`, and the target `dialect`. Definitions are + * stored separately from the root schema so they can be relocated when + * converting between dialects. + * - **MultiDocument** — same as `Document` but carries multiple root + * schemas (at least one). Useful when generating several schemas that + * share a single definitions pool. + * - **Definitions** — a `Record` keyed by definition + * name. The ref pointer prefix depends on the dialect. + * - **`from*` functions** — parse a raw JSON Schema object into the + * canonical `Document<"draft-2020-12">`. + * - **`to*` functions** — convert from the canonical representation to a + * specific output dialect. + * + * ## Common tasks + * + * - Parse a Draft-07 schema → {@link fromSchemaDraft07} + * - Parse a Draft-2020-12 schema → {@link fromSchemaDraft2020_12} + * - Parse an OpenAPI 3.1 schema → {@link fromSchemaOpenApi3_1} + * - Parse an OpenAPI 3.0 schema → {@link fromSchemaOpenApi3_0} + * - Convert to Draft-07 output → {@link toDocumentDraft07} + * - Convert to OpenAPI 3.1 output → {@link toMultiDocumentOpenApi3_1} + * - Resolve a `$ref` against definitions → {@link resolve$ref} + * - Inline the root `$ref` of a document → {@link resolveTopLevel$ref} + * + * ## Gotchas + * + * - All `from*` functions normalize to `Document<"draft-2020-12">` + * regardless of the input dialect. + * - Unsupported or unrecognized JSON Schema keywords are silently dropped + * during conversion. + * - Draft-07 tuple syntax (`items` as array + `additionalItems`) is + * converted to 2020-12 form (`prefixItems` + `items`), and vice-versa. + * - OpenAPI 3.0 `nullable: true` is expanded into `type` arrays or + * `anyOf` unions. The `nullable` keyword is removed. + * - OpenAPI 3.0 singular `example` is converted to `examples` (array). + * - {@link resolve$ref} only looks up the last segment of the ref path in + * the definitions map; it does not follow arbitrary JSON Pointer paths. + * + * ## Quickstart + * + * **Example** (Parse a Draft-07 schema and convert to Draft-07 output) + * + * ```ts + * import { JsonSchema } from "effect" + * + * const raw: JsonSchema.JsonSchema = { + * type: "object", + * properties: { + * name: { type: "string" } + * }, + * required: ["name"] + * } + * + * // Parse into canonical form + * const doc = JsonSchema.fromSchemaDraft07(raw) + * + * // Convert back to Draft-07 + * const draft07 = JsonSchema.toDocumentDraft07(doc) + * + * console.log(draft07.dialect) // "draft-07" + * console.log(draft07.schema) // { type: "object", properties: { name: { type: "string" } }, required: ["name"] } + * ``` + * + * ## See also + * + * - {@link Document} + * - {@link MultiDocument} + * - {@link fromSchemaDraft07} + * - {@link toDocumentDraft07} + * - {@link resolve$ref} + * + * @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 to populate the `$schema` field when 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 JSON Schema 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 schema 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 consuming schemas 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 consuming schemas 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 schema 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 generating an OpenAPI 3.1 specification from internal schemas. + * + * **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 actual + * schema 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 to dereference a top-level `$ref` before inspecting the root + * schema'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-smol/packages/effect/src/Latch.ts b/.repos/effect-smol/packages/effect/src/Latch.ts new file mode 100644 index 00000000000..9a9ac36d84a --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Latch.ts @@ -0,0 +1,398 @@ +/** + * The `Latch` module provides a reusable synchronization primitive for + * coordinating fibers. A `Latch` is either open or closed: when it is closed, + * fibers that use {@link _await await} or {@link whenOpen} suspend until the latch is + * opened or the current waiters are released. + * + * **Mental model** + * + * - An open latch lets current and future waiters continue immediately + * - A closed latch causes `await` and `whenOpen` to suspend + * - {@link open} permanently opens the latch until it is closed again + * - {@link release} wakes only the fibers currently waiting and leaves the + * latch closed for future waiters + * - {@link close} resets the latch so later waiters suspend again + * + * **Common tasks** + * + * - Create a latch inside `Effect`: {@link make} + * - Create a latch synchronously: {@link makeUnsafe} + * - Wait for a signal before continuing: {@link _await await} + * - Guard an effect so it runs only after the latch is open: {@link whenOpen} + * - Let all current and future waiters proceed: {@link open} + * - Let only the current waiters proceed: {@link release} + * - Re-enable waiting after opening: {@link close} + * + * **Gotchas** + * + * - `release` is not the same as `open`; new waiters still suspend after the + * current waiters are released + * - `open` and `close` report whether they changed the latch state + * - Prefer the effectful APIs unless synchronous allocation or mutation is + * required + * + * @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 +} + +/** + * Creates a `Latch` synchronously, outside of `Effect`. + * + * **When to use** + * + * Use when synchronous allocation is required 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 synchronous code needs 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 diff --git a/.repos/effect-smol/packages/effect/src/Layer.ts b/.repos/effect-smol/packages/effect/src/Layer.ts new file mode 100644 index 00000000000..992299e5ea2 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Layer.ts @@ -0,0 +1,2813 @@ +/** + * The `Layer` module provides the dependency-injection building blocks for + * Effect applications. A {@link Layer} describes how to acquire one or more + * services, which dependencies are needed to acquire them, and which errors can + * occur during acquisition. + * + * **Mental model** + * + * - Application effects ask for services through context tags. + * - Layer code builds those services, often from configuration, clients, + * connection pools, or other services. + * - The application boundary provides a final layer to the program. + * - Layers are lazy: acquisition starts only when a layer is provided, built, or + * launched. + * - Layer acquisition is scoped, so finalizers run when the owning scope closes. + * - A layer value is memoized by default; reusing the same layer value shares + * the acquired service instance. + * + * **Common tasks** + * + * - Provide an existing service value with {@link succeed}. + * - Build a service lazily with {@link sync}, {@link effect}, or + * {@link effectContext}. + * - Run setup work that provides no services with {@link effectDiscard}. + * - Combine independent layers with {@link merge} or {@link mergeAll}. + * - Feed one layer's output into another layer's requirements with + * {@link provide}. + * - Keep dependency services in the final output with {@link provideMerge}. + * - Materialize a layer manually with {@link build} or {@link buildWithScope}. + * + * **Gotchas** + * + * - Sharing is tied to layer identity. Constructing the same layer twice creates + * two distinct values and can acquire two service instances. + * - Use {@link fresh} when a layer must be rebuilt even if the same value is + * provided more than once. + * - Scoped resources belong in layers when construction and release are part of + * the service lifecycle. + * - Normal application code should request services; layer code should create + * services. + * + * @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 type { ErrorWithStackTraceLimit } from "./internal/tracer.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 getOrCreate: (self: Context.Context) => MemoMap = Context.getOrElse( + this, + 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.getOrCreate(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.getOrCreate(fiber.context), + scope + ) + )) + +/** + * Constructs a layer that provides a single service from an already available + * value. + * + * **When to use** + * + * Use when the service implementation is already constructed and does + * not need effectful acquisition. Use `sync` when the service should be created + * lazily during layer construction. + * + * **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 already have a `Context` or need to provide + * multiple services at once. Use `succeed` when you only need to provide one + * service value. + * + * **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 when you use `Layer.empty` as the no-op branch when conditionally composing layers. + * If you need to run an effect during layer construction while still providing + * no services, use `effectDiscard`. + * + * **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 the service can be created synchronously but should be + * deferred until the layer is built. Use `succeed` when the service value is + * already available. + * + * **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 multiple services can be created synchronously and + * should be deferred until the layer is built. Use `sync` when you only need to + * provide one service. + * + * **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 constructing the service requires effects, dependencies, or + * scoped resource acquisition. Use `effectContext` when the effect produces + * multiple services in a `Context`, and `effectDiscard` when construction work + * should provide no services. + * + * **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 effectful construction needs to provide multiple + * services at once. Use `effect` when the effect produces one service value. + * + * **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 this is useful when you want to run an Effect for its side effects during + * layer construction, but don't need to provide any 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 utils + * @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 when composing from an existing layer in a pipeline. Use + * `mergeAll` when you already have all layers as separate arguments. + * + * **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 the dependency layer is an implementation detail of the + * layer being built and should not be exposed to callers. Use `provideMerge` + * when callers should also receive the dependency services. + * + * **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 utils + * @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 callers need access to both the service being built and the + * dependency used to build it, such as a health check that needs both a + * repository and its database. Prefer `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 utils + * @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 construction error should use the same recovery + * path. Use `catchTag` to recover from specific tagged errors, and `catchCause` + * when recovery needs the full failure cause. + * + * @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 construction errors should be recovered. + * Use `catchCause` when recovery depends on defects, interruption, or other + * cause information. + * + * **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 recovery needs more than the typed error, such as + * defects or interruption information. Use `catchTag` when recovery only needs + * to match specific tagged errors. + * + * **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 utils + * @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 two parts of an application must receive separate instances + * of a resource, such as two independent client sessions. 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 utils + * @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 your entire application is 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 = (Error as ErrorWithStackTraceLimit).stackTraceLimit + ;(Error as ErrorWithStackTraceLimit).stackTraceLimit = 2 + const error = new Error(`${service.key}: Unimplemented method "${prop.toString()}"`) + ;(Error as ErrorWithStackTraceLimit).stackTraceLimit = 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 models + * @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** (Using 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-smol/packages/effect/src/LayerMap.ts b/.repos/effect-smol/packages/effect/src/LayerMap.ts new file mode 100644 index 00000000000..4d319e168ea --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/LayerMap.ts @@ -0,0 +1,493 @@ +/** + * The `LayerMap` module provides utilities for managing scoped resources that + * are selected by key and built from `Layer` values. A `LayerMap` turns + * a key into a cached service `Context`, so applications can lazily acquire + * and reuse different resource instances such as tenant clients, regional + * connections, environment-specific services, or other keyed infrastructure. + * + * **Mental model** + * + * - A `LayerMap` is a scoped, reference-counted cache of contexts produced by layers + * - Keys identify which layer-backed resource set should be acquired + * - Resources are acquired on demand when a key is requested + * - The same key reuses the cached context while it remains live + * - Cached resources are finalized when invalidated, when their scope closes, or after idle expiration + * - The layers built by a `LayerMap` share the current layer memoization map + * + * **Common tasks** + * + * - Create from a lookup function: {@link make} + * - Create from a fixed record of layers: {@link fromRecord} + * - Define a service wrapper with accessor helpers: {@link Service} + * - Retrieve a layer for a key: {@link LayerMap.get} + * - Retrieve a scoped context directly: {@link LayerMap.contextEffect} + * - Force a cached entry to be rebuilt later: {@link LayerMap.invalidate} + * - Remove idle entries automatically with the `idleTimeToLive` option + * - Eagerly build known entries with `preloadKeys` or `preload` + * + * **Gotchas** + * + * - `contextEffect` requires a `Scope.Scope` because it exposes the acquired context directly + * - `get` returns a `Layer` that can be provided to programs expecting the keyed services + * - Invalidating a key finalizes the current cached resources for that key; the next access rebuilds them + * - Preloading moves layer construction errors to `LayerMap` creation instead of first use + * + * @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 * 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.getOrCreate(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 = Err.stackTraceLimit + Err.stackTraceLimit = 2 + const creationError = new Err() + Err.stackTraceLimit = limit + + function TagClass() {} + const TagClass_ = TagClass as any as Mutable> + Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.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-smol/packages/effect/src/LogLevel.ts b/.repos/effect-smol/packages/effect/src/LogLevel.ts new file mode 100644 index 00000000000..60d35a1e55b --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/LogLevel.ts @@ -0,0 +1,443 @@ +/** + * The `LogLevel` module defines the levels used by Effect logging and the + * ordering operations used to compare, filter, and enable log output. + * + * **Mental model** + * + * - A `LogLevel` is one of `All`, `Fatal`, `Error`, `Warn`, `Info`, `Debug`, + * `Trace`, or `None` + * - `Fatal` is the most severe concrete level and `Trace` is the least severe + * - `All` and `None` are sentinel levels: `All` enables every message and + * `None` disables every message + * - Ordering follows logging severity, so higher levels are more important and + * lower levels are more verbose + * - Filtering is usually expressed as "log this message when its level is + * greater than or equal to the configured minimum" + * + * **Common tasks** + * + * - Enumerate levels with {@link values} + * - Compare exact levels with {@link Equivalence} + * - Sort or compare by severity with {@link Order} and {@link getOrdinal} + * - Check thresholds with {@link isGreaterThanOrEqualTo} and + * {@link isLessThanOrEqualTo} + * - Test whether a level is enabled for the current fiber with + * {@link isEnabled} + * + * **Gotchas** + * + * - `All` and `None` are useful for configuration boundaries, but they are not + * concrete message severities; use {@link Severity} when only emitted message + * levels are valid + * - The comparison helpers compare severity, not declaration position in source + * code or alphabetical order + * - `isEnabled` reads the current fiber's `MinimumLogLevel` reference, so it is + * context-sensitive; use the pure comparison helpers when checking an + * explicit threshold + * + * @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-smol/packages/effect/src/Logger.ts b/.repos/effect-smol/packages/effect/src/Logger.ts new file mode 100644 index 00000000000..e2d67d696e5 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Logger.ts @@ -0,0 +1,1361 @@ +/** + * The `Logger` module defines the logging model used by the Effect runtime and + * provides constructors for formatting, routing, batching, and installing + * loggers. A `Logger` receives each runtime log event as an + * {@link Options} value and transforms it into an output such as a string, + * structured object, JSON line, console write, file write, or trace span event. + * + * **Mental model** + * + * - Effect programs emit log events with APIs such as `Effect.log`, + * `Effect.logInfo`, `Effect.logWarning`, and `Effect.logError` + * - Each event contains a message, log level, cause, fiber, and timestamp + * - Loggers are ordinary values created with {@link make} and installed with + * {@link layer} + * - Multiple loggers can be active at once by providing a layer with several + * logger values + * - Formatter loggers such as {@link formatLogFmt}, {@link formatStructured}, + * and {@link formatJson} return formatted data without writing it anywhere + * - Console loggers such as {@link consolePretty}, {@link consoleLogFmt}, + * {@link consoleStructured}, and {@link consoleJson} write formatted output + * to the active Effect console + * + * **Log output structure** + * + * Built-in formatters include the log level, timestamp, fiber identifier, and + * logged message. When present, they also include the pretty-printed cause, + * active log annotations, and active log spans. Structured and JSON loggers keep + * these fields as machine-readable data, while logfmt and pretty loggers render + * them as human-readable text. + * + * **Common tasks** + * + * - Create a custom logger: {@link make} + * - Transform logger output: {@link map} + * - Write formatter output to the console: {@link withConsoleLog}, + * {@link withConsoleError}, {@link withLeveledConsole} + * - Use built-in console loggers: {@link consolePretty}, {@link consoleLogFmt}, + * {@link consoleStructured}, {@link consoleJson} + * - Use built-in formatter loggers: {@link formatSimple}, {@link formatLogFmt}, + * {@link formatStructured}, {@link formatJson} + * - Batch logger output before flushing to a sink: {@link batched} + * - Write string logger output to a file: {@link toFile} + * - Preserve trace correlation by including {@link tracerLogger} + * - Install or replace loggers for an effect: {@link layer} + * + * **Gotchas** + * + * - {@link layer} replaces the current logger set by default; pass + * `mergeWithExisting: true` when adding loggers to the existing runtime + * loggers + * - Formatter loggers only produce values; wrap them with console, file, batch, + * or custom sink loggers when output should be written somewhere + * - {@link batched} and {@link toFile} are scoped; keep their scope open while + * logs are being emitted so buffered entries can flush reliably + * - {@link toFile} accepts only loggers that output strings, so pair it with + * string formatters such as {@link formatJson} or {@link formatLogFmt} + * - The default runtime logger set includes {@link tracerLogger}; replacing + * loggers without merging may remove automatic log-to-trace-span recording + * + * **Quickstart** + * + * **Example** (Installing a JSON console logger) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.logInfo("request started", { method: "GET", path: "/users" }) + * yield* Effect.logError("request failed", { status: 500 }) + * }).pipe( + * Effect.annotateLogs("service", "users-api"), + * Effect.withLogSpan("http.request"), + * Effect.provide(Logger.layer([Logger.consoleJson])) + * ) + * ``` + * + * **See also** + * + * - {@link make} for defining custom loggers + * - {@link layer} for installing loggers + * - {@link formatJson} and {@link consoleJson} for structured production logs + * - {@link consolePretty} for readable local logs + * + * @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 models + * @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 keep stdout reserved for protocol messages or data output while still + * allowing Effect runtime logs to be emitted. + * + * **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 this allows you to modify, enhance, or completely change the output format + * of an existing logger without recreating the entire 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 utils + * @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 this is useful for taking any logger that produces string or object output + * and routing it to the console for development or debugging purposes. + * + * **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 utils + * @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 this is particularly useful for error logging where you want to ensure + * log messages appear in the error stream (stderr) rather than standard output. + * + * **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 utils + * @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` -> `console.debug` + * - `Info` -> `console.info` + * - `Trace` -> `console.trace` + * - `Warn` -> `console.warn` + * - `Error` and `Fatal` -> `console.error` + * - Others -> `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 utils + * @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** (Using 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-smol/packages/effect/src/ManagedRuntime.ts b/.repos/effect-smol/packages/effect/src/ManagedRuntime.ts new file mode 100644 index 00000000000..100914215f6 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/ManagedRuntime.ts @@ -0,0 +1,411 @@ +/** + * The `ManagedRuntime` module provides a way to build a reusable runtime from + * a `Layer` and use it to run effects that require the services produced by + * that layer. A `ManagedRuntime` owns the lifecycle of the layer-built + * resources, caches the resulting `Context`, and exposes runners for + * integrating Effect programs with JavaScript entry points. + * + * **Mental model** + * + * - A managed runtime is created from a `Layer` with {@link make} + * - The layer is built lazily the first time the runtime is used + * - The built context is cached and reused for subsequent effect executions + * - Resources acquired by the layer are owned by the runtime's internal scope + * - Disposing the runtime closes that scope and releases all managed resources + * - Effects run through the runtime receive the layer's services automatically + * + * **Common tasks** + * + * - Create a runtime from application services: {@link make} + * - Run an effect as a `Promise`: {@link ManagedRuntime.runPromise} + * - Run an effect and keep its `Exit`: {@link ManagedRuntime.runPromiseExit} + * - Fork an effect into a `Fiber`: {@link ManagedRuntime.runFork} + * - Bridge callback-style APIs: {@link ManagedRuntime.runCallback} + * - Run synchronous effects at program boundaries: {@link ManagedRuntime.runSync}, + * {@link ManagedRuntime.runSyncExit} + * - Access the cached service context: {@link ManagedRuntime.context} + * - Release layer resources: {@link ManagedRuntime.dispose}, + * {@link ManagedRuntime.disposeEffect} + * + * **Gotchas** + * + * - Always dispose a managed runtime when it is no longer needed, especially + * when the layer acquires resources such as connections, servers, or files + * - Layer construction errors are included in the error channel of runtime + * runners, so `ER` is combined with the effect's own error type + * - `runSync` can only execute effects without asynchronous boundaries; use + * `runPromise` for asynchronous programs + * - After disposal, the runtime cannot be reused + * + * @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 type-level + * @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 type-level + * @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-smol/packages/effect/src/Match.ts b/.repos/effect-smol/packages/effect/src/Match.ts new file mode 100644 index 00000000000..f116599d54b --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Match.ts @@ -0,0 +1,2640 @@ +/** + * Pattern matching for TypeScript values, predicates, and tagged unions. + * + * `Match` turns branching logic into a matcher that is built from ordered + * cases and finished with an explicit finalizer. Use `Match.type` to define a + * reusable matcher for a type, or `Match.value` to classify one value + * immediately. Cases can match literal values, predicates, object patterns, + * discriminators, tags, or negated patterns. + * + * **Mental model** + * + * A matcher checks cases in the order they are added and evaluates the handler + * for the first match. Type matchers produce a function that can be reused with + * different inputs, while value matchers already contain the input value. As + * cases are added, the type system tracks which inputs remain unmatched, so + * `Match.exhaustive` is only available when every remaining case has been + * handled. + * + * **Common tasks** + * + * - Use `Match.type()` when a branch table should be reusable and + * exhaustiveness-checked. + * - Use `Match.value(value)` when a single value should be matched immediately. + * - Use `Match.tag`, `Match.tags`, or `Match.discriminator` for discriminated + * unions and domain objects with tag fields. + * - Use `Match.orElse`, `Match.option`, or `Match.result` when unmatched input + * should be handled explicitly instead of requiring full exhaustiveness. + * + * **Example** (Matching a tagged union) + * + * ```ts + * import { Match } from "effect" + * + * type Event = + * | { readonly _tag: "UserCreated"; readonly id: string } + * | { readonly _tag: "UserDeleted"; readonly id: string } + * + * const describe = Match.type().pipe( + * Match.tag("UserCreated", (event) => `created ${event.id}`), + * Match.tag("UserDeleted", (event) => `deleted ${event.id}`), + * Match.exhaustive + * ) + * ``` + * + * @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 Creating a matcher + * @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 Creating a matcher + * @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 Creating a matcher + * @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 Creating a matcher + * @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 utils + * @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 when a match branch should handle only the literal `null` value. + * + * **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 constructor matching needs 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-smol/packages/effect/src/Metric.ts b/.repos/effect-smol/packages/effect/src/Metric.ts new file mode 100644 index 00000000000..4ec62cb0006 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Metric.ts @@ -0,0 +1,4141 @@ +/** + * The `Metric` module provides tools for defining, updating, tagging, and + * reading application metrics from Effect programs. A `Metric` + * accepts typed input values and aggregates them into a typed state that can be + * read directly or exported from a snapshot. + * + * **Mental model** + * + * - A metric has an identifier, a type, an optional description, optional attributes, and mutable aggregate state + * - Use counters for cumulative values such as requests, errors, retries, or bytes processed + * - Use gauges for point-in-time values that can rise or fall, such as active connections or queue size + * - Use frequencies to count occurrences of discrete string values, such as status codes or action names + * - Use histograms to bucket numeric observations and inspect count, min, max, and sum + * - Use summaries to calculate quantiles over a bounded, time-based observation window + * - Metrics are updated from effects with {@link update} and {@link modify}, and read with {@link value} + * - Attributes tag metrics with key-value dimensions so the same logical metric can be grouped by service, endpoint, method, or other labels + * - Snapshots capture the currently registered metrics and their aggregate states for reporting or export + * + * **Common tasks** + * + * - Create counters: {@link counter} + * - Create gauges: {@link gauge} + * - Create frequencies: {@link frequency} + * - Create histograms: {@link histogram}, {@link linearBoundaries}, {@link exponentialBoundaries} + * - Create summaries: {@link summary}, {@link summaryWithTimestamp} + * - Measure effect duration: {@link timer} + * - Update a metric: {@link update} + * - Apply relative updates where supported: {@link modify} + * - Read one metric: {@link value} + * - Tag a metric: {@link withAttributes} + * - Transform accepted input values: {@link mapInput} + * - Record a constant input for repeated events: {@link withConstantInput} + * - Inspect all registered metrics: {@link snapshot}, {@link dump} + * - Enable fiber runtime metrics: {@link enableRuntimeMetrics} + * + * **Gotchas** + * + * - Counter and gauge metrics can use `number` inputs by default or `bigint` inputs with the `bigint` option + * - Incremental counters ignore negative updates; use non-incremental counters only when decreases are meaningful + * - {@link update} sets a gauge to an absolute value, while {@link modify} changes it relative to its current value + * - Histogram buckets are cumulative and depend on the boundaries supplied when the metric is created + * - Summary quantiles are calculated from the configured sliding window, so old observations expire + * - Prefer low-cardinality attributes; using unbounded values such as request IDs can create too many metric series + * + * **Quickstart** + * + * **Example** (Creating and updating metrics) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * const requestCount = Metric.counter("http_requests_total", { + * description: "Total number of HTTP requests" + * }) + * + * const responseTime = Metric.histogram("http_response_time", { + * description: "HTTP response time in milliseconds", + * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 20 }) + * }) + * + * const handleRequest = Effect.gen(function*() { + * yield* Metric.update( + * Metric.withAttributes(requestCount, { + * endpoint: "/api/users", + * method: "GET" + * }), + * 1 + * ) + * + * yield* Metric.update(responseTime, 125) + * + * return yield* Metric.value(requestCount) + * }) + * ``` + * + * **See also** + * + * - {@link counter} / {@link gauge} / {@link frequency} for common metric types + * - {@link histogram} / {@link summary} for distribution metrics + * - {@link update} / {@link modify} / {@link value} for working with metric state + * - {@link withAttributes} for adding dimensions + * - {@link snapshot} for exporting all registered metric values + * + * @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** (Using the current 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 to provide a custom metric registry when a program or test needs metrics + * isolated from 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** + * + * - `description` - A description of the `Counter`. + * - `attributes` - The attributes to associate with the `Counter`. + * - `bigint` - Indicates if the `Counter` should use the `bigint` type. + * - `incremental` - Set 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 gauges are most suitable for metrics that represent instantaneous values, + * such as memory usage or CPU load. + * + * **Details** + * + * - `description` - A description of the `Gauge`. + * - `attributes` - The attributes to associate with the `Gauge`. + * - `bigint` - Indicates if the `Gauge` should use the `bigint` type. + * + * **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 frequency metrics are most suitable for counting the number of times a + * specific event or incident occurs. + * + * **Details** + * + * - `description` - A description of the `Frequency`. + * - `attributes` - The attributes to associate with the `Frequency`. + * - `preregisteredWords` - Occurrences which are pre-registered with the + * `Frequency` metric occurrences. + * + * **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 histogram metrics are most suitable for measuring the distribution of values + * within a range. + * + * **Details** + * + * - `description` - A description of the `Histogram`. + * - `attributes` - The attributes to associate with the `Histogram`. + * - `boundaries` - The bucket boundaries of the `Histogram` + * + * **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 summary metrics are most suitable for providing statistical information about + * a set of values, including quantiles. + * + * **Details** + * + * - `description` - An description of the `Summary`. + * - `attributes` - The attributes to associate with the `Summary`. + * - `maxAge` - The maximum age of observations to retain. + * - `maxSize` - The maximum number of observations to keep. + * - `quantiles` - An array of quantiles to calculate (e.g., [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 summary metrics are most suitable for statistical information about a set of + * values. + * + * **Details** + * + * Inputs to this metric are `[value, timestamp]` pairs; the current clock is + * used when reading quantiles against the configured `maxAge`. + * + * - `description` - An description of the `Summary`. + * - `attributes` - The attributes to associate with the `Summary`. + * - `maxAge` - The maximum age of observations to retain. + * - `maxSize` - The maximum number of observations to keep. + * - `quantiles` - An array of quantiles to calculate (e.g., [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: + * + * - Counter: `CounterState` with `count` and `incremental` + * - Gauge: `GaugeState` with `value` + * - Frequency: `FrequencyState` with `occurrences` + * - Histogram: `HistogramState` with buckets, count, min, max, and sum + * - Summary: `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 utils + * @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: + * + * - **Counter**: Adds the input value to the current count + * - **Gauge**: Adds the input value to the current gauge value + * - **Frequency**: Same as `update` - increments the occurrence count for the input string + * - **Histogram**: Same as `update` - records the input value in the appropriate bucket + * - **Summary**: Same as `update` - records 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 utils + * @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: + * + * - **Counter**: Adds the input value to the current count (same as `modify`) + * - **Gauge**: Sets the gauge to the specified value (replaces current value) + * - **Frequency**: Increments the occurrence count for the input string by 1 + * - **Histogram**: Records the input value in the appropriate bucket + * - **Summary**: Records 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 utils + * @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** (Using 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** (Using 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 this is useful when you want to selectively disable runtime metrics for specific + * parts of your application while keeping them enabled elsewhere, or when you need + * to avoid the overhead of metrics collection in performance-critical sections. + * + * **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-smol/packages/effect/src/MutableHashMap.ts b/.repos/effect-smol/packages/effect/src/MutableHashMap.ts new file mode 100644 index 00000000000..0d9eee59e0b --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/MutableHashMap.ts @@ -0,0 +1,871 @@ +/** + * `MutableHashMap` is an in-place key-value map with 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` / + * `Hash`, so callers can mix referential and structural lookup in the same + * collection. + * + * **Mental model** + * + * - `MutableHashMap` stores entries on a single mutable map instance + * - {@link set}, {@link remove}, and {@link clear} mutate that instance and + * return it for convenient piping + * - Keys that implement `Equal` / `Hash` are matched structurally through hash + * buckets; other keys use JavaScript map semantics + * - The map is iterable as `[key, value]` pairs and reports size in O(1) + * + * **Common tasks** + * + * - Create maps: {@link empty}, {@link make}, {@link fromIterable} + * - Read entries: {@link get}, {@link has}, {@link size} + * - Mutate entries: {@link set}, {@link modify}, {@link remove}, {@link clear} + * - Narrow unknown values: {@link isMutableHashMap} + * + * **Gotchas** + * + * - This data structure is intentionally mutable; share it only when callers + * agree on ownership + * - Mutating a structural key after insertion can make future lookups fail if + * its equality or hash changes + * - Iteration follows the underlying storage order and should not be used as a + * sorting guarantee + * + * **Performance** + * + * - Lookup, insertion, removal, clearing, and size are O(1) on average + * - Hash collisions can make key lookup and removal O(n) + * - Iteration is O(n) + * + * @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 the 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 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 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 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-smol/packages/effect/src/MutableHashSet.ts b/.repos/effect-smol/packages/effect/src/MutableHashSet.ts new file mode 100644 index 00000000000..42800eda2d7 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/MutableHashSet.ts @@ -0,0 +1,480 @@ +/** + * `MutableHashSet` is an in-place hash set for storing unique values with fast + * membership checks, insertion, removal, clearing, and iteration. It is built on + * {@link 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. + * + * **Mental model** + * + * - `MutableHashSet` is a mutable collection of unique values of type `V` + * - {@link add}, {@link remove}, and {@link clear} mutate the same set instance + * - Duplicate values are ignored according to Effect equality and hashing + * semantics + * - Values that implement `Equal` / `Hash` can be compared structurally; + * primitive values and ordinary object references use the underlying hash map + * behavior + * - The set is iterable, so `for...of` and `Array.from(set)` can inspect the + * current values + * + * **Common tasks** + * + * - Create an empty set: {@link empty} + * - Create from values or an iterable: {@link make}, {@link fromIterable} + * - Add, remove, and clear values: {@link add}, {@link remove}, {@link clear} + * - Check membership and size: {@link has}, {@link size} + * - Narrow unknown values: {@link isMutableHashSet} + * + * **Gotchas** + * + * - This data structure is intentionally mutable; keep ownership clear if the + * same set is shared by multiple callers + * - Mutating operations return the same set instance for convenient piping + * - Do not use iteration order as a sorting or presentation guarantee + * - Use immutable collection modules when callers need persistent snapshots + * + * **Performance** + * + * - Add, membership checks, and removal are O(1) on average + * - Hash collisions can make those operations O(n) + * - Clearing and reading size are O(1) + * - Iteration is O(n) + * + * **Quickstart** + * + * **Example** (Tracking unique values) + * + * ```ts + * import { MutableHashSet } from "effect" + * + * const set = MutableHashSet.make("alice", "bob", "alice") + * + * MutableHashSet.add(set, "carol") + * MutableHashSet.remove(set, "bob") + * + * console.log(MutableHashSet.has(set, "alice")) + * // Output: true + * + * console.log(MutableHashSet.size(set)) + * // Output: 2 + * ``` + * + * **See also** + * + * - {@link MutableHashMap} for the mutable key-value structure underneath + * - {@link empty}, {@link make}, and {@link fromIterable} for construction + * - {@link add}, {@link remove}, and {@link has} for core set operations + * + * @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-smol/packages/effect/src/MutableList.ts b/.repos/effect-smol/packages/effect/src/MutableList.ts new file mode 100644 index 00000000000..a65dc3387e1 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/MutableList.ts @@ -0,0 +1,992 @@ +/** + * The `MutableList` module provides a mutable linked list for accumulating, + * ordering, inspecting, and draining values with efficient operations at both + * ends of the list. + * + * A `MutableList` stores values in linked buckets of arrays. Appending adds + * values to the tail, prepending adds values to the head, and taking removes + * values from the head. Unlike persistent collections, every mutation updates + * the list object in place: operations such as {@link append}, {@link prepend}, + * {@link take}, {@link takeN}, {@link clear}, {@link filter}, and {@link remove} + * change the same `MutableList` instance and update its `length`. + * + * **Mental model** + * + * - `MutableList` is a stateful container with `head`, `tail`, and `length` + * - Values are consumed from the head with {@link take}, {@link takeN}, or + * {@link takeAll} + * - {@link append} and {@link appendAll} preserve FIFO queue order for normal + * producer-consumer use cases + * - {@link prepend} and {@link prependAll} place values before the current + * contents, which is useful for priority work or restoring items to the front + * - {@link toArray} and {@link toArrayN} copy values without modifying the list + * - The `head` and `tail` bucket fields are exposed for advanced use, but most + * code should treat them as implementation details + * + * **Common tasks** + * + * - Create an empty list: {@link make} + * - Add one value: {@link append}, {@link prepend} + * - Add many values: {@link appendAll}, {@link prependAll} + * - Drain one value: {@link take} + * - Drain many values: {@link takeN}, {@link takeAll} + * - Inspect without draining: {@link toArrayN}, {@link toArray} + * - Reset the list: {@link clear} + * - Mutate contents in place: {@link filter}, {@link remove} + * + * **Gotchas** + * + * - `MutableList` is intentionally mutable; sharing a list means sharing its + * changing state + * - {@link take} returns the {@link Empty} symbol when the list has no value, so + * compare with `MutableList.Empty` instead of relying on falsy checks + * - {@link appendAllUnsafe} and {@link prependAllUnsafe} may reuse the provided + * array when `mutable` is `true`; only enable that optimization when callers + * will not keep using the array independently + * - {@link remove} uses JavaScript strict equality semantics, not structural + * equality + * + * @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. + * + * **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. + * + * **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. + * + * **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-smol/packages/effect/src/MutableRef.ts b/.repos/effect-smol/packages/effect/src/MutableRef.ts new file mode 100644 index 00000000000..821a0230385 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/MutableRef.ts @@ -0,0 +1,839 @@ +/** + * The `MutableRef` module provides a small synchronous container for mutable + * state. A `MutableRef` stores one current value of type `A`, exposes that + * value through `.current`, and offers pipeable helpers for reading, replacing, + * and transforming the value in place. + * + * **Mental model** + * + * - `MutableRef` is a stable reference whose `.current` field may change over time + * - Reads and writes are synchronous and return immediately + * - `set`, `update`, `increment`, `decrement`, and `toggle` mutate the same reference in place + * - `getAnd*` helpers return the previous value, while `*AndGet` helpers return the new value + * - `compareAndSet` updates only when the current value is equal to the expected value using `Equal.equals` + * - A `MutableRef` is useful for local mutable state, but it does not make updates transactional or effectful + * + * **Common tasks** + * + * - Create a reference: {@link make} + * - Read the current value: {@link get} or `.current` + * - Replace the current value: {@link set}, {@link setAndGet}, {@link getAndSet} + * - Transform the current value: {@link update}, {@link updateAndGet}, {@link getAndUpdate} + * - Coordinate conditional replacement: {@link compareAndSet} + * - Work with counters: {@link increment}, {@link decrement}, {@link incrementAndGet}, {@link decrementAndGet} + * - Work with boolean flags: {@link toggle} + * + * **Gotchas** + * + * - All updates are imperative mutations; aliases to the same `MutableRef` observe the same changing value + * - Updating object or array values does not clone them unless the update function creates a new value + * - `compareAndSet` compares with Effect equality semantics, not only JavaScript reference equality + * - For state that must participate in `Effect` workflows, interruption, or fiber coordination, prefer higher-level Effect data types + * + * @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 mutable reference 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 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 to decrement a numeric reference in place when you want the same + * reference back. + * + * **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 reference 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 value without mutating the reference. + * + * **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 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 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 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 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 to increment a numeric reference in place when you want the same + * reference back. + * + * **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 reference 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 to replace the current value in place when you want the same reference + * back. + * + * **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 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 to transform the current value in place when you want the same reference + * back. + * + * **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 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 to flip a boolean reference in place when you want the same reference + * back. + * + * **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-smol/packages/effect/src/Newtype.ts b/.repos/effect-smol/packages/effect/src/Newtype.ts new file mode 100644 index 00000000000..fbdb4cc4758 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Newtype.ts @@ -0,0 +1,369 @@ +/** + * Lightweight wrapper types that prevent accidental mixing of structurally + * identical values (e.g. `UserId` vs `OrderId`, both `string` at runtime). + * + * **Mental model** + * + * - **Newtype** — a compile-time wrapper around a **carrier** type (the + * underlying primitive or object). At runtime the value is unchanged; the + * tag exists only in the type system. + * - **Key** — a unique string literal that distinguishes one newtype from + * another (e.g. `"Label"`, `"UserId"`). + * - **Carrier** — the underlying type the newtype wraps (e.g. `string`, + * `number`). + * - **Iso** — a lossless two-way conversion between a newtype and its carrier, + * created with {@link makeIso}. Use `iso.set(carrier)` to wrap and + * `iso.get(newtype)` to unwrap. + * + * **Common tasks** + * + * - Define a newtype → declare an `interface` extending + * `Newtype.Newtype` + * - Wrap / unwrap values → {@link makeIso} (returns an `Optic.Iso`) + * - Unwrap only → {@link value} + * - Lift an `Equivalence` → {@link makeEquivalence} + * - Lift an `Order` → {@link makeOrder} + * - Lift a `Combiner` → {@link makeCombiner} + * - Lift a `Reducer` → {@link makeReducer} + * + * **Gotchas** + * + * - Newtypes are **purely compile-time**. There is zero runtime overhead; + * `value` and `makeIso` use identity casts. + * - Two newtypes sharing the same key string will be assignable to each other. + * Choose unique key strings. + * - A newtype value is **not** assignable to its carrier type without + * explicitly unwrapping via {@link value} or an iso. + * + * **Quickstart** + * + * **Example** (defining and using a newtype) + * + * ```ts + * import { Newtype } from "effect" + * + * // 1. Define a newtype + * interface Label extends Newtype.Newtype<"Label", string> {} + * + * // 2. Create an iso for wrapping/unwrapping + * const labelIso = Newtype.makeIso` can be consumed anywhere an `Iterable` is expected, + * while also carrying the guarantee that reading the first element is safe. + * + * **Mental model** + * + * - `NonEmptyIterable` is an `Iterable` branded with a non-empty guarantee + * - The guarantee is static: values should only be typed this way when construction or validation proves at least one element exists + * - The iterable can be an array, string, set, map, generator, or any custom iterable + * - `unprepend` safely separates the first element from an iterator for the remaining elements + * - Operations that may remove elements, such as filtering, usually return ordinary collections because they can become empty + * + * **Common tasks** + * + * - Accept inputs that must contain at least one value + * - Extract a head element and process the remaining iterator with {@link unprepend} + * - Model APIs such as reductions, comparisons, or aggregation that are undefined for empty inputs + * - Preserve compatibility with the JavaScript iteration protocol while documenting the stronger invariant + * + * **Gotchas** + * + * - A type assertion does not make an empty iterable non-empty; only assert after a trusted check or constructor + * - Iterators are stateful, so calling {@link unprepend} consumes the first yielded value from that iterator + * - The order of the first element follows the source iterable's iteration order, for example insertion order for `Map` and `Set` + * - Some transformations preserve non-emptiness, but transformations that can discard elements must account for the empty case + * + * **Quickstart** + * + * **Example** (Requiring a non-empty iterable) + * + * ```ts + * import { NonEmptyIterable } from "effect" + * + * // NonEmptyIterable is a type that represents any iterable with at least one element + * function processNonEmpty(data: NonEmptyIterable.NonEmptyIterable): A { + * // Safe to get the first element - guaranteed to exist + * const [first] = NonEmptyIterable.unprepend(data) + * return first + * } + * + * // Using Array.make to create non-empty arrays + * const numbers = Array.make( + * 1, + * 2, + * 3, + * 4, + * 5 + * ) as unknown as NonEmptyIterable.NonEmptyIterable + * const firstNumber = processNonEmpty(numbers) // number + * + * // Regular arrays can be asserted as NonEmptyIterable when known to be non-empty + * const values = [1, 2, 3] as unknown as NonEmptyIterable.NonEmptyIterable + * const firstValue = processNonEmpty(values) // number + * + * // Custom iterables that are guaranteed non-empty + * function* generateNumbers(): NonEmptyIterable.NonEmptyIterable { + * yield 1 + * yield 2 + * yield 3 + * } + * + * const firstGenerated = processNonEmpty(generateNumbers()) // number + * ``` + * + * ## Working with Different Iterable Types + * + * **Example** (Adapting iterable inputs) + * + * ```ts + * import { Array } from "effect" + * + * // Creating non-empty arrays + * const nonEmptyArray = Array.make( + * 1, + * 2, + * 3 + * ) as unknown as NonEmptyIterable.NonEmptyIterable + * + * // Working with strings (assert as NonEmptyIterable when known to be non-empty) + * const nonEmptyString = "hello" as unknown as NonEmptyIterable.NonEmptyIterable< + * string + * > + * const [firstChar] = NonEmptyIterable.unprepend(nonEmptyString) + * console.log(firstChar) // "h" + * + * // Working with Maps (assert when known to be non-empty) + * const nonEmptyMap = new Map([ + * ["key1", "value1"], + * ["key2", "value2"] + * ]) as unknown as NonEmptyIterable.NonEmptyIterable<[string, string]> + * const [firstEntry] = NonEmptyIterable.unprepend(nonEmptyMap) + * console.log(firstEntry) // ["key1", "value1"] + * + * // Custom generator functions + * function* fibonacci(): NonEmptyIterable.NonEmptyIterable { + * let a = 1, b = 1 + * yield a + * while (true) { + * yield b + * const next = a + b + * a = b + * b = next + * } + * } + * + * const [firstFib, restFib] = NonEmptyIterable.unprepend( + * fibonacci() as unknown as NonEmptyIterable.NonEmptyIterable + * ) + * console.log(firstFib) // 1 + * ``` + * + * ## Integration with Effect Arrays + * + * **Example** (Processing non-empty iterables with Array) + * + * ```ts + * import { Array, pipe } from "effect" + * import type { NonEmptyIterable } from "effect" + * + * // Many Array functions work with NonEmptyIterable + * declare const nonEmptyData: NonEmptyIterable.NonEmptyIterable + * + * const processData = pipe( + * nonEmptyData, + * Array.fromIterable, + * Array.map((x) => x * 2), + * Array.filter((x) => x > 5) + * // Result is a regular array since filtering might make it empty + * ) + * + * // Safe operations that preserve non-emptiness + * const doubledData = pipe( + * nonEmptyData, + * Array.fromIterable, + * Array.map((x) => x * 2) + * // This would still be non-empty if the source was non-empty + * ) + * ``` + * + * @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-smol/packages/effect/src/Number.ts b/.repos/effect-smol/packages/effect/src/Number.ts new file mode 100644 index 00000000000..6cf4701fba9 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Number.ts @@ -0,0 +1,907 @@ +/** + * Operations for working with TypeScript `number` values. Use this module for + * arithmetic, safe parsing and division, comparisons, range checks, clamping, + * rounding, and reducers for numeric aggregation. + * + * **Mental model** + * + * Numbers remain plain JavaScript numbers, including `NaN`, `Infinity`, and + * floating-point behavior. The module adds named operations around that runtime + * model: arithmetic such as {@link sum}, {@link multiply}, {@link subtract}, + * and {@link remainder}; safety wrappers such as {@link parse} and + * {@link divide}; range helpers such as {@link between} and {@link clamp}; and + * instances such as {@link Order} and {@link Equivalence}. + * + * **Common tasks** + * + * - Coerce, parse, or narrow input: {@link Number}, {@link parse}, + * {@link isNumber} + * - Do arithmetic: {@link sum}, {@link multiply}, {@link subtract}, + * {@link divide}, {@link divideUnsafe}, {@link remainder}, {@link round} + * - Work with counters and powers: {@link increment}, {@link decrement}, + * {@link nextPow2} + * - Compare and bound values: {@link isLessThan}, {@link isGreaterThan}, + * {@link between}, {@link clamp}, {@link min}, {@link max}, {@link sign} + * - Aggregate iterables or reducer inputs: {@link sumAll}, {@link multiplyAll}, + * {@link ReducerSum}, {@link ReducerMultiply}, {@link ReducerMax}, + * {@link ReducerMin} + * + * **Gotchas** + * + * - {@link Number} is the native JavaScript constructor. For example, + * `Number.Number("")` returns `0`; {@link parse} returns `Option.none()` for + * blank strings and invalid numeric text. + * - {@link divide} returns `Option.none()` only when the divisor is `0`. + * Other JavaScript number results, including `NaN`, still follow normal + * number semantics. + * - {@link Equivalence} treats `NaN` as equivalent to `NaN`, unlike `===`. + * - Reducers have identity values: {@link ReducerSum} starts at `0`, + * {@link ReducerMultiply} starts at `1`, {@link ReducerMax} starts at + * `-Infinity`, and {@link ReducerMin} starts at `Infinity`. + * + * **Quickstart** + * + * **Example** (Parsing and bounding a number) + * + * ```ts + * import { Number } from "effect" + * + * const parsed = Number.parse("42") + * console.log(parsed) // Option.some(42) + * + * const bounded = Number.clamp(120, { minimum: 0, maximum: 100 }) + * console.log(bounded) // 100 + * + * const total = Number.sumAll([1, 2, 3]) + * console.log(total) // 6 + * ``` + * + * @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) +) + +/** + * Provides an unsafe division operation on `number`s that throws a `RangeError` if the divisor is `0`. + * + * **When to use** + * + * Use when the divisor is known to be non-zero and division by zero should be a + * thrown exception. + * + * **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 sorting or comparing 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 selfDecCount = decimalCount(self) + const divisorDecCount = decimalCount(divisor) + 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 decimalCount(n: number): number { + const s = n.toString() + const eIndex = s.indexOf("e-") + if (eIndex !== -1) { + const exp = parseInt(s.slice(eIndex + 2)) + const mantissaDecimals = (s.slice(0, eIndex).split(".")[1] || "").length + return mantissaDecimals + exp + } + return (s.split(".")[1] || "").length +} + +/** + * 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-smol/packages/effect/src/Optic.ts b/.repos/effect-smol/packages/effect/src/Optic.ts new file mode 100644 index 00000000000..3073ec1182e --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Optic.ts @@ -0,0 +1,1780 @@ +/** + * Composable, immutable accessors for reading and updating nested data + * structures without mutation. + * + * **Mental model** + * + * - **Optic** — a first-class reference to a piece inside a larger structure. + * Compose optics to reach deeply nested values. + * - **Iso** — lossless two-way conversion (`get`/`set`) between `S` and `A`. + * Extends both {@link Lens} and {@link Prism}. + * - **Lens** — focuses on exactly one part of `S`. `get` always succeeds; + * `replace` needs the original `S` to produce the updated whole. + * - **Prism** — focuses on a part that may not be present (e.g. a union + * variant). `getResult` can fail; `set` builds a new `S` from `A` alone. + * - **Optional** — the most general optic: both reading and writing can fail. + * - **Traversal** — focuses on zero or more elements of an array-like + * structure. Technically `Optional>`. + * - **Hierarchy** (strongest → weakest): + * `Iso > Lens | Prism > Optional`. Composing a weaker optic with any other + * produces the weaker kind. + * + * **Common tasks** + * + * - Start a chain → {@link id} (identity iso) + * - Drill into a struct key → `.key("name")` / `.optionalKey("name")` + * - Drill into a key that may not exist → `.at("name")` + * - Narrow a tagged union → `.tag("MyVariant")` + * - Narrow by type guard → `.refine(guard)` + * - Add validation → `.check(Schema.isGreaterThan(0))` + * - Filter out `undefined` → `.notUndefined()` + * - Pick/omit struct keys → `.pick(["a","b"])` / `.omit(["c"])` + * - Traverse array elements → `.forEach(el => el.key("field"))` + * - Build an iso → {@link makeIso} + * - Build a lens → {@link makeLens} + * - Build a prism → {@link makePrism}, {@link fromChecks} + * - Build an optional → {@link makeOptional} + * - Focus into `Option.Some` → {@link some} + * - Focus into `Result.Success`/`Failure` → {@link success}, {@link failure} + * - Convert record ↔ entries → {@link entries} + * - Extract all traversal elements → {@link getAll} + * + * **Gotchas** + * + * - Updates are structurally persistent: only nodes on the path are cloned. + * Unrelated branches keep referential identity. However, **no-op updates + * may still allocate** a new root — do not rely on reference identity to + * detect no-ops. + * - `replace` silently returns the original `S` when the optic cannot focus + * (e.g. wrong tag). Use `replaceResult` for explicit failure. + * - `modify` also returns the original `S` on focus failure — it never throws. + * - `.key()` and `.optionalKey()` do not work on union types (compile error). + * - Only plain objects (`Object.prototype` or `null` prototype) and arrays can + * be cloned. Class instances cause a runtime error on `replace`/`modify`. + * + * **Quickstart** + * + * **Example** (reading and updating nested state) + * + * ```ts + * import { Optic } from "effect" + * + * type State = { user: { name: string; age: number } } + * + * const _age = Optic.id().key("user").key("age") + * + * const s1: State = { user: { name: "Alice", age: 30 } } + * + * // Read + * console.log(_age.get(s1)) + * // Output: 30 + * + * // Update immutably + * const s2 = _age.replace(31, s1) + * console.log(s2) + * // Output: { user: { name: "Alice", age: 31 } } + * + * // Modify with a function + * const s3 = _age.modify((n) => n + 1)(s1) + * console.log(s3) + * // Output: { user: { name: "Alice", age: 31 } } + * + * // Referential identity is preserved for unrelated branches + * console.log(s2.user !== s1.user) + * // Output: true (on the path) + * ``` + * + * **See also** + * + * - {@link id} — entry point for optic chains + * - {@link Lens} / {@link Prism} / {@link Optional} — core optic types + * - {@link Traversal} / {@link getAll} — multi-focus optics + * - {@link some} / {@link success} / {@link failure} — built-in prisms + * + * @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 AST from "./SchemaAST.ts" +import type * as Issue 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** (Celsius ↔ 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 functions that form a lossless round-trip between `S` + * and `A`. + * + * **Details** + * + * - The returned optic can be composed with any other optic. + * + * **Example** (wrapping/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 (the part exists unconditionally). + * - You need the original `S` to produce the updated whole (unlike + * {@link 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** (lens into 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** (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 [AST.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 [AST.Check, ...Array>] + + constructor(checks: readonly [AST.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** (record key that may be absent) + * + * ```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** (only focus 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 [AST.Check, ...Array>]): Prism + check(this: Optional, ...checks: readonly [AST.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** (safe record access) + * + * ```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) + * + * ```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 both reading and writing can fail. + * + * **Details** + * + * - `getResult` should return `Result.fail(message)` on mismatch. + * - `set` should return `Result.fail(message)` when the update cannot be + * applied. + * + * **Example** (safe record key access) + * + * ```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 [AST.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([AST.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(AST.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 starting 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) => AST.runChecks([AST.makeFilterByGuard(refinement, annotations)], e) as any +} diff --git a/.repos/effect-smol/packages/effect/src/Option.ts b/.repos/effect-smol/packages/effect/src/Option.ts new file mode 100644 index 00000000000..e2a7f6cbe18 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Option.ts @@ -0,0 +1,2707 @@ +/** + * The `Option` module provides a type-safe way to represent values that may or + * may not exist. An `Option` is either `Some` (containing a value) or + * `None` (representing absence). + * + * **Mental model** + * + * - `Option` is a discriminated union: `None | Some` + * - `None` represents the absence of a value (like `null`/`undefined`, but type-safe) + * - `Some` wraps a present value of type `A`, accessed via `.value` + * - `Option` is a monad: chain operations with {@link flatMap}, compose pipelines with `pipe` + * - All operations are pure and return new `Option` values; the input is never mutated + * - `Option` is yieldable in `Effect.gen`, producing the inner value or short-circuiting with `NoSuchElementError` + * + * **Common tasks** + * + * - Create from a value: {@link some}, {@link none} + * - Create from nullable: {@link fromNullishOr}, {@link fromNullOr}, {@link fromUndefinedOr} + * - Create from iterable: {@link fromIterable} + * - Create from Result: {@link getSuccess}, {@link getFailure} + * - Transform: {@link map}, {@link flatMap}, {@link andThen} + * - Unwrap: {@link getOrElse}, {@link getOrNull}, {@link getOrUndefined}, {@link getOrThrow} + * - Pattern match: {@link match} + * - Fallbacks: {@link orElse}, {@link orElseSome}, {@link firstSomeOf} + * - Filter: {@link filter}, {@link filterMap} + * - Combine multiple: {@link all}, {@link zipWith}, {@link product} + * - Generator syntax: {@link gen} + * - Do notation: {@link Do}, {@link bind}, {@link let_ let} + * - Check contents: {@link isSome}, {@link isNone}, {@link contains}, {@link exists} + * + * **Gotchas** + * + * - `Option.some(null)` is a valid `Some`; use {@link fromNullishOr} to treat `null`/`undefined` as `None` + * - {@link filterMap} uses a `Filter` callback that returns `Result` + * - {@link getOrThrow} throws a generic `Error`; prefer {@link getOrThrowWith} for custom errors + * - `None` is a singleton; compare with {@link isNone}, not `===` + * - When yielded in `Effect.gen`, a `None` becomes a `NoSuchElementError` defect + * + * **Quickstart** + * + * **Example** (Working with optional values) + * + * ```ts + * import { Option } from "effect" + * + * const name = Option.some("Alice") + * const age = Option.none() + * + * // Transform + * const upper = Option.map(name, (s) => s.toUpperCase()) + * + * // Unwrap with fallback + * console.log(Option.getOrElse(upper, () => "unknown")) + * // Output: "ALICE" + * + * console.log(Option.getOrElse(age, () => 0)) + * // Output: 0 + * + * // Combine multiple options + * const both = Option.all({ name, age }) + * console.log(Option.isNone(both)) + * // Output: true + * ``` + * + * **See also** + * + * - {@link some} / {@link none} for creating values + * - {@link map} / {@link flatMap} for transforming values + * - {@link match} for pattern matching + * - {@link gen} for generator-based syntax + * + * @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 to represent `Option` in higher-kinded type operations. + * + * @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 branching on absence 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 branching on presence 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 exhaustively handling both branches in one expression + * - Transforming 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 turning a parsing function into a type-narrowing predicate + * - Filtering arrays with `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 safely extracting the head of a collection + * - Working with 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 discarding the failure channel when you only care about success + * + * **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 to extract the failure when you do not need the success value + * + * **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 chaining fallback `Option` computations + * - 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 distinguishing whether a 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 searching for the first available value in 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 bridging from nullable APIs to `Option` + * - Wrapping values that may be `null` or `undefined` + * + * **Details** + * + * - `null` or `undefined` → `None` + * - Any other value → `Some` (typed as `NonNullable`) + * + * **Example** (From nullable values) + * + * ```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 when `null` is a meaningful value but `undefined` means absent + * + * **Details** + * + * - `undefined` → `None` + * - Any other value (including `null`) → `Some` + * + * **Example** (From possibly-undefined values) + * + * ```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 when `undefined` is a meaningful value but `null` means absent + * + * **Details** + * + * - `null` → `None` + * - Any other value (including `undefined`) → `Some` + * + * **Example** (From possibly-null values) + * + * ```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 interoping with APIs that use `null` for missing values + * + * **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 interoping with APIs that use `undefined` for missing values + * + * **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 fail-fast unwrapping when absence is unexpected + * - Providing a descriptive error for debugging + * + * **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 quick fail-fast unwrapping when 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 optional 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 preserving presence/absence while discarding the original value + * + * **Details** + * + * - `Some` → `Some(b)` + * - `None` → `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 discarding the value while preserving presence/absence + * + * **Details** + * + * - `Some` → `Some(undefined)` + * - `None` → `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** (Using 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 chaining dependent optional 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 flexible chaining where the next step may return `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 chaining with functions that use `null`/`undefined` instead of `Option` + * - Navigating deeply nested optional properties + * + * **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 removing one layer of `Option` nesting + * + * **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 to run a side-condition that must succeed, then using 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 to run a validation that must succeed, but keeping the original 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 to build pipelines of partial functions (Kleisli composition) + * + * **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 a value without transforming it + * - 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 pairing two optional values together + * + * **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 collecting several `Option`s of the same type into 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 collecting multiple `Option`s into one, preserving the input shape + * - "All or nothing" combination — any `None` makes 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 merging two optional 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 aggregating 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 interfacing with array-based APIs + * - Spreading 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 categorizing an optional value into "left" (failure) and "right" (success) channels + * + * **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 a 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 discarding values that don't meet a condition + * - 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 to compare two `Option` values for structural equality + * + * **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 Equivalence + * @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 to sort collections of `Option` values + * + * **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 reusing an existing binary function in an `Option` context + * + * **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 testing membership with a custom equality check + * + * **Details** + * + * - `Some` where `isEquivalent(value, a)` is `true` → `true` + * - `Some` where not equivalent, or `None` → `false` + * + * **Example** (Custom equivalence check) + * + * ```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 quick membership test with 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 beginning a 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 binding a derived (non-`Option`) value in a 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 sequencing `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 starting a do notation pipeline before adding bindings + * + * **Example** (Do notation pipeline) + * + * ```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 writing imperative-style code that chains multiple `Option`s + * - Readability when many sequential optional steps are involved + * + * **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** (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 a reducer that falls back to the first available value + * - Combining optional values where 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 operations that require both values to be present + * + * **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 to wrap an existing `Reducer` to work with `Option` values + * - Reductions where any `None` should abort the entire result + * + * **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-smol/packages/effect/src/Order.ts b/.repos/effect-smol/packages/effect/src/Order.ts new file mode 100644 index 00000000000..4d620f5b182 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Order.ts @@ -0,0 +1,1105 @@ +/** + * The `Order` module defines total orderings: pure comparison functions that + * decide whether one value is less than, equal to, or greater than another. An + * `Order` returns a normalized {@link Ordering} (`-1`, `0`, or `1`), making + * it suitable for sorting, finding minimum and maximum values, range checks, and + * building ordered data structures. + * + * **Mental model** + * + * - An {@link Order} is a comparator with laws: totality, antisymmetry, and + * transitivity. If those laws do not hold, sorting and range operations can + * produce surprising results. + * - `-1` means the left value comes before the right value, `0` means they are + * equal for this ordering, and `1` means the left value comes after the right + * value. + * - Primitive orders such as {@link Number}, {@link String}, {@link Boolean}, + * {@link BigInt}, and {@link Date} are building blocks. + * - Use {@link mapInput} to compare larger values by a field or derived key. + * - Use {@link combine} or {@link combineAll} for tie-breaking, where the first + * non-zero comparison result wins. + * + * **Common tasks** + * + * - Create a custom order from a comparison function with {@link make}. + * - Sort or compare using built-in orders such as {@link Number} and + * {@link String}. + * - Compare records and tuples with {@link Struct} and {@link Tuple}. + * - Compare arrays lexicographically with {@link Array}. + * - Convert an order into predicates with {@link isLessThan}, + * {@link isGreaterThan}, {@link isLessThanOrEqualTo}, and + * {@link isGreaterThanOrEqualTo}. + * - Select boundaries with {@link min}, {@link max}, {@link clamp}, and + * {@link isBetween}. + * + * **Gotchas** + * + * - {@link make} returns `0` immediately when `self === that`; the custom + * comparison function is not called for identical references. + * - {@link Number} treats all `NaN` values as equal to each other and less than + * every non-`NaN` number. + * - {@link Array} compares elements first and length second. {@link Tuple} + * compares a fixed number of positions. + * - {@link Struct} compares fields in the key order of the object passed to it, + * so put the highest-priority fields first. + * - {@link min} and {@link max} return the first argument when two values + * compare as equal. + * + * **Example** (Sorting by multiple fields) + * + * ```ts + * import { Array, Order } from "effect" + * + * interface User { + * readonly name: string + * readonly age: number + * } + * + * const byAge = Order.mapInput(Order.Number, (user: User) => user.age) + * const byName = Order.mapInput(Order.String, (user: User) => user.name) + * const byAgeThenName = Order.combine(byAge, byName) + * + * const users = [ + * { name: "Charlie", age: 30 }, + * { name: "Bob", age: 25 }, + * { name: "Alice", age: 30 } + * ] + * + * const sorted = Array.sort(users, byAgeThenName) + * console.log(sorted.map((user) => user.name)) + * // ["Bob", "Alice", "Charlie"] + * ``` + * + * **See also** + * + * - {@link Ordering} for the normalized comparison result type. + * - `Equivalence` for equality without less-than or greater-than. + * - {@link Reducer} for combining orders with reducer-style APIs. + * + * @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 when you need to define how values of a type should be compared + * - When implementing sorting, searching, or ordered data structures + * - When composing multiple comparison criteria + * + * **Details** + * + * - Returns `-1` if the first value is less than the second + * - Returns `0` if the values are equal according to this ordering + * - Returns `1` if the first value is greater than the second + * - Must satisfy total ordering laws (totality, antisymmetry, transitivity) + * + * **Example** (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 when working with type-level operations that require higher-kinded types + * - When implementing generic type classes that work with orders + * + * **Details** + * + * - Type-level only: no runtime representation + * - 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 when creating a custom order for a type that doesn't have a built-in order + * - When you need fine-grained control over comparison logic + * - When implementing orders for complex types + * + * **Details** + * + * - Uses reference equality (`===`) as a shortcut: if `self === that`, returns `0` without calling the comparison function + * - The comparison function should return `-1`, `0`, or `1` based on the comparison result + * - The returned order satisfies total ordering laws if 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 when comparing strings alphabetically + * - When sorting string collections + * - As a base for creating orders on types containing strings + * + * **Details** + * + * - Uses lexicographic (dictionary) ordering + * - Empty string is less than any non-empty string + * - Comparison is case-sensitive + * + * **Example** (String Ordering) + * + * ```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 when comparing numbers for sorting or searching + * - As a base for creating orders on types containing numbers + * - When implementing numeric comparisons in data structures + * + * **Details** + * + * - `0` is considered equal to `-0` + * - All `NaN` values are considered equal to each other + * - Any `NaN` is considered less than any non-NaN number + * - Uses standard numeric comparison for all other values + * + * **Example** (Number Ordering) + * + * ```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 when comparing booleans for sorting or searching + * - As a base for creating orders on types containing booleans + * - When implementing boolean-based comparisons + * + * **Details** + * + * - `false` is less than `true` + * - Equal values return `0` + * + * **Example** (Boolean Ordering) + * + * ```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 when comparing bigint values for sorting or searching + * - As a base for creating orders on types containing bigints + * - When working with large integers that exceed number precision + * + * **Details** + * + * - Uses standard numeric comparison for bigint values + * - Handles arbitrarily large integers + * + * **Example** (BigInt Ordering) + * + * ```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 when you need descending order instead of ascending + * - When reversing an existing order without modifying the original + * - When creating orders that compare in the opposite direction + * + * **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 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 when you need multi-criteria comparison (e.g., sort by age, then by name) + * - When creating composite orders from simpler orders + * - When implementing lexicographic ordering + * + * **Details** + * + * - First applies the first order; if the result is non-zero, returns that result + * - If the first order returns `0` (equal), applies the second order + * - Returns the first non-zero result, or `0` if both orders return `0` + * + * **Example** (Combining 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 when you need an order that doesn't distinguish between values + * - As a default or fallback order when no meaningful comparison exists + * - When implementing optional ordering where equality is sufficient + * + * **Details** + * + * - Always returns `0` regardless of input values + * - Useful as a neutral element in order composition + * + * **Example** (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 when you have a variable number of orders to combine + * - When combining orders from a collection or array + * - When implementing dynamic multi-criteria sorting + * + * **Details** + * + * - Applies orders in iteration order + * - Returns the first non-zero result from any order + * - Returns `0` only if all orders return `0` + * - Short-circuits on the first non-zero result + * + * **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 when you have an order for a property type and want to compare objects by that property + * - When extracting a comparable value from a complex type + * - When creating orders for types that contain comparable values + * + * **Details** + * + * - Applies the mapping function to both values before comparison + * - The mapping function should be pure and not have side effects + * - Preserves the ordering properties of the original order + * + * **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 when comparing dates for sorting or searching + * - As a base for creating orders on types containing dates + * - When implementing time-based comparisons + * + * **Details** + * + * - Compares dates by their underlying timestamp (milliseconds since epoch) + * - Earlier dates are less than later dates + * - Invalid dates are compared as if they were valid (uses `getTime()` result) + * + * **Example** (Date Ordering) + * + * ```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 when comparing tuples with different types for each position + * - When you need type-safe tuple ordering + * - When working with fixed-length heterogeneous collections + * + * **Details** + * + * - Compares tuples element-by-element using the corresponding order + * - Stops at the first non-zero comparison result + * - Requires tuples to have the same length as the order collection + * - Returns `0` if all elements are equal + * + * **Example** (Tuple Ordering) + * + * ```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 when comparing arrays of the same element type + * - When you want shorter arrays to be considered less than longer arrays + * - When sorting collections of arrays + * + * **Details** + * + * - Compares arrays element-by-element using the provided order + * - Stops at the first non-zero comparison result + * - If all elements are equal, shorter arrays are less than longer arrays + * - Returns `0` only if arrays have the same length and all elements are equal + * + * **Example** (Array Element Ordering) + * + * ```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 when comparing objects with multiple properties + * - When you need multi-field comparison for structs + * - When creating orders for complex data types + * + * **Details** + * + * - Compares structs field-by-field in the order of keys in the fields object + * - Stops at the first non-zero comparison result + * - Returns `0` only if all fields are equal + * - Field order matters: earlier fields take precedence + * + * **Example** (Struct Ordering) + * + * ```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 when you need a boolean predicate instead of an ordering result + * - When checking if a value is less than another in conditional logic + * - When implementing range checks or comparisons + * + * **Details** + * + * - Returns `true` if the order returns `-1` (first value is less than second) + * - Returns `false` for equal or greater values + * - Supports curried and uncurried call styles + * + * **Example** (Less Than) + * + * ```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 when you need a boolean predicate instead of an ordering result + * - When checking if a value is greater than another in conditional logic + * - When implementing range checks or comparisons + * + * **Details** + * + * - Returns `true` if the order returns `1` (first value is greater than second) + * - Returns `false` for equal or lesser values + * - Supports curried and uncurried call styles + * + * **Example** (Greater Than) + * + * ```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 when you need a boolean predicate for non-strict comparison + * - When checking if a value is within a range (inclusive lower bound) + * - When implementing inclusive comparisons + * + * **Details** + * + * - Returns `true` if the order returns `-1` or `0` (less than or equal) + * - Returns `false` only if the order returns `1` (greater than) + * - Supports curried and uncurried call styles + * + * **Example** (Less Than Or Equal) + * + * ```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 when you need a boolean predicate for non-strict comparison + * - When checking if a value is within a range (inclusive upper bound) + * - When implementing inclusive comparisons + * + * **Details** + * + * - Returns `true` if the order returns `1` or `0` (greater than or equal) + * - Returns `false` only if the order returns `-1` (less than) + * - Supports curried and uncurried call styles + * + * **Example** (Greater Than Or Equal) + * + * ```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 when you need to find the smaller of two values + * - When implementing min/max operations + * - When selecting values based on ordering + * + * **Details** + * + * - Returns the value that compares as less than or equal to the other + * - If values are equal, returns the first argument + * - Supports curried and uncurried call styles + * + * **Example** (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 when you need to find the larger of two values + * - When implementing min/max operations + * - When selecting values based on ordering + * + * **Details** + * + * - Returns the value that compares as greater than or equal to the other + * - If values are equal, returns the first argument + * - Supports curried and uncurried call styles + * + * **Example** (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 when you need to restrict a value to a specific range + * - When implementing bounds checking and normalization + * - When ensuring values stay within valid ranges + * + * **Details** + * + * - Returns the value if it's between minimum and maximum (inclusive) + * - Returns minimum if the value is less than minimum + * - Returns maximum if the value is greater than maximum + * - Supports curried and uncurried call styles + * - Requires that minimum <= 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 when validating that a value is within a valid range + * - When implementing range checks for bounds validation + * - When filtering or selecting values within a range + * + * **Details** + * + * - Returns `true` if the value is greater than or equal to minimum and less than or equal to maximum + * - Returns `false` if the value is outside the range + * - Supports curried and uncurried call styles + * - Both bounds are inclusive + * + * **Example** (Checking Range) + * + * ```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 when you need to combine multiple orders from a collection using reducer patterns + * - When implementing fold operations over collections of orders + * - When working with reducers that operate on orders + * + * **Details** + * + * - Returns a reducer that combines orders using {@link combine} + * - Uses {@link alwaysEqual} as the identity element (returns `0` for empty collections) + * - Uses {@link 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 utils + * @since 4.0.0 + */ +export function makeReducer() { + return Reducer.make>( + combine, + () => 0, + combineAll + ) +} diff --git a/.repos/effect-smol/packages/effect/src/Ordering.ts b/.repos/effect-smol/packages/effect/src/Ordering.ts new file mode 100644 index 00000000000..6cbe44ec9bc --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Ordering.ts @@ -0,0 +1,208 @@ +/** + * The `Ordering` module provides the standard representation for the result of + * comparing two values. An `Ordering` is one of three numeric literals: `-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. + * + * **Mental model** + * + * - `Ordering` describes the relationship between two compared values, not the + * values themselves + * - Negative means "less than", zero means "equal", and positive means "greater + * than" + * - Unlike JavaScript comparators, this type is normalized to exactly `-1`, `0`, + * or `1` + * - `0` is neutral when combining comparisons; the first non-zero ordering + * determines the result + * + * **Common tasks** + * + * - Interpret a comparison result with {@link match} + * - Reverse ascending and descending order with {@link reverse} + * - Combine multiple comparison criteria with {@link Reducer} + * - Build custom comparison functions for sorting, ordered collections, and + * domain-specific ordering rules + * + * **Gotchas** + * + * - Do not cast arbitrary comparator results such as `a.localeCompare(b)` + * directly unless they have been normalized to `-1`, `0`, or `1` + * - In comparator-style APIs, `-1` means the left value should come before the + * right value, while `1` means it should come after + * - Reversing an `Ordering` swaps `-1` and `1`, but leaves `0` unchanged + * + * @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-smol/packages/effect/src/PartitionedSemaphore.ts b/.repos/effect-smol/packages/effect/src/PartitionedSemaphore.ts new file mode 100644 index 00000000000..ed82a96504a --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/PartitionedSemaphore.ts @@ -0,0 +1,589 @@ +/** + * 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. + * + * **Mental model** + * + * - The semaphore has a fixed shared capacity measured in permits + * - Work acquires permits with a partition key of type `K` + * - Waiting acquisitions are tracked per partition + * - Released permits are assigned to waiting partitions in round-robin order + * - `withPermit` and `withPermits` acquire permits around an effect and + * release them when the effect exits, fails, or is interrupted + * + * **Common tasks** + * + * - Create a semaphore: {@link make}, {@link makeUnsafe} + * - Inspect capacity and availability: {@link capacity}, {@link available} + * - Acquire and release manually: {@link take}, {@link release} + * - Limit a single operation per partition: {@link withPermit} + * - Limit weighted work per partition: {@link withPermits} + * - Run only when permits are immediately available: + * {@link withPermitsIfAvailable} + * + * **Gotchas** + * + * - `withPermitsIfAvailable` does not use a partition key; it only succeeds + * when the shared pool has enough permits immediately + * - Acquiring more permits than the semaphore capacity never completes + * - Requests for zero or negative permits complete without acquiring anything + * - Non-finite capacities create an unbounded semaphore whose acquire and + * release operations complete immediately + * + * @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 a partitioned semaphore must be constructed 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 to manually acquire permits for a partition when acquisition and release + * must be controlled 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 to manually return permits acquired with `take` when a lower-level + * partitioned permit protocol needs 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-smol/packages/effect/src/Path.ts b/.repos/effect-smol/packages/effect/src/Path.ts new file mode 100644 index 00000000000..8d60f7e8d1d --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Path.ts @@ -0,0 +1,892 @@ +/** + * The `Path` module provides a platform path service for manipulating file + * system paths through Effect's environment. It models path operations as a + * replaceable service so programs can depend on path behavior without directly + * coupling to a particular runtime implementation. + * + * **Mental model** + * + * - `Path.Path` is a `Context.Service` tag used to access the current path implementation + * - The service offers familiar path operations such as joining, resolving, parsing, and formatting + * - Most operations are pure string transformations and follow POSIX-style path semantics + * - File URL conversions return `Effect`s because invalid paths or URLs can fail with `BadArgument` + * - Custom implementations can be provided with `Layer.succeed` for alternate platforms or tests + * + * **Common tasks** + * + * - Combine path segments with `join` or turn segments into an absolute path with `resolve` + * - Normalize `.` and `..` segments with `normalize` + * - Inspect paths with `basename`, `dirname`, `extname`, and `isAbsolute` + * - Convert between structured path parts and strings with `parse` and `format` + * - Compute relative paths with `relative` + * - Convert between file paths and `file:` URLs with `toFileUrl` and `fromFileUrl` + * + * **Gotchas** + * + * - Path strings are not checked against the file system; these operations only manipulate syntax + * - `resolve` may consult the host current working directory when no absolute segment is supplied + * - `fromFileUrl` only accepts valid `file:` URLs and rejects encoded path separators + * - Use the service from the environment when writing portable Effect code instead of importing + * host-specific path APIs directly + * + * @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 an effect needs path operations supplied by its 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 tag + * @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 an effect requires the `Path` service and should 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-smol/packages/effect/src/Pipeable.ts b/.repos/effect-smol/packages/effect/src/Pipeable.ts new file mode 100644 index 00000000000..8e0fec15383 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Pipeable.ts @@ -0,0 +1,685 @@ +/** + * 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. + * + * **Common tasks** + * + * - Type values that expose a `.pipe(...)` method with the {@link Pipeable} interface + * - Implement a custom `.pipe(...)` method with {@link pipeArguments} + * - Reuse the standard implementation through {@link Prototype}, {@link Class}, or {@link Mixin} + * + * **Gotchas** + * + * - Each function receives the result of the previous function, not the original value + * - The overloads preserve precise types for long pipelines, but very long chains may be easier to read when split + * + * @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 utils + * @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 extend or compose this constructor when defining a class that should support + * 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-smol/packages/effect/src/PlatformError.ts b/.repos/effect-smol/packages/effect/src/PlatformError.ts new file mode 100644 index 00000000000..e04e6d7e50c --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/PlatformError.ts @@ -0,0 +1,243 @@ +/** + * The `PlatformError` module defines the normalized error model used by + * platform APIs when adapting host operations into Effect programs. It gives + * callers a stable `PlatformError` wrapper whose `reason` is either a + * `BadArgument`, for invalid inputs rejected before an operation runs, or a + * `SystemError`, for failures reported by the host platform or operating + * system. + * + * Use this module when implementing or consuming platform services such as + * file systems, terminal access, sockets, or other environment-specific APIs. + * `SystemError` intentionally groups many low-level failures into a small set + * of portable tags like `NotFound`, `PermissionDenied`, and `TimedOut`, while + * still preserving operation details such as the module, method, syscall, path + * or descriptor, description, and original cause when available. + * + * **Common tasks** + * + * - Create platform failures from system operations with {@link systemError} + * - Report rejected caller input with {@link badArgument} + * - Inspect the underlying reason via {@link PlatformError.reason} + * - Match normalized system failures with {@link SystemErrorTag} + * + * **Gotchas** + * + * - `PlatformError` is a wrapper; inspect `reason` to distinguish + * `BadArgument` from `SystemError` + * - `SystemErrorTag` values are normalized categories, not necessarily raw + * platform error codes + * - The original cause is preserved when provided, but portable handling + * should rely on the normalized fields + * + * @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 a platform API rejects caller input before performing the underlying + * operation and callers need invalid-argument reason data directly. + * + * **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 as the reason data for failures reported by a host platform or operating + * system when you need a normalized system error tag plus 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-smol/packages/effect/src/Pool.ts b/.repos/effect-smol/packages/effect/src/Pool.ts new file mode 100644 index 00000000000..bf0cfe5fd19 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Pool.ts @@ -0,0 +1,737 @@ +/** + * Scoped resource pools for sharing expensive or limited resources across + * fibers. + * + * The `Pool` module acquires values with a scoped effect, lets fibers borrow + * them with {@link get}, and releases all acquired values when the pool scope + * closes. Pools are useful for database connections, clients, buffers, or other + * resources where acquisition is expensive and total concurrency must be + * bounded. + * + * **Mental model** + * + * - A pool owns resources between its configured minimum and maximum size. + * - Each item is acquired in the pool scope and finalized when removed or when + * the pool shuts down. + * - {@link get} checks out an item in the caller's scope; leaving that scope + * returns the item to the pool. + * - `concurrency` is per item, so a pool with size 4 and concurrency 2 can + * serve up to 8 simultaneous checkouts. + * - `targetUtilization` controls when the resize strategy grows or shrinks the pool. + * + * **Common tasks** + * + * - Create a fixed-size pool with {@link make}. + * - Create an elastic pool that expires idle items with {@link makeWithTTL}. + * - Use {@link makeWithStrategy} for custom resize or reclamation behavior. + * - Borrow items safely in scoped effects with {@link get}. + * - Replace a broken item lazily with {@link invalidate}. + * + * **Gotchas** + * + * - Pool construction requires `Scope`; closing that scope shuts down the pool. + * - A checked-out item is returned only when the checkout scope closes, so do + * not leak scoped effects that hold pool items indefinitely. + * - Acquisition failures fail the {@link get} effect for that checkout; later + * checkouts can try acquisition again. + * - Invalidated items are finalized when they are no longer checked out, then + * replacement happens on demand. + * + * **See also** + * + * - {@link Pool} for the pool value. + * - {@link get} for checked-out access. + * - {@link invalidate} for removing a specific item. + * + * @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 to create a fixed-size pool when you know the exact number of resources + * needed upfront, without 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** (Create 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 you determine it is no + * longer suitable, 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-smol/packages/effect/src/Predicate.ts b/.repos/effect-smol/packages/effect/src/Predicate.ts new file mode 100644 index 00000000000..f8993b5d41e --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Predicate.ts @@ -0,0 +1,1922 @@ +/** + * Predicates are small boolean functions for checking values at runtime. + * Refinements are predicates that also narrow TypeScript types after a + * successful check. This module provides primitive guards for common JavaScript + * values and combinators for building larger checks from smaller ones. + * + * **Mental model** + * + * - A `Predicate` is `(a: A) => boolean` + * - A `Refinement` is `(a: A) => a is B`; when it returns `true`, + * TypeScript can treat the value as `B` + * - Guards such as {@link isString}, {@link isNumber}, and {@link isObject} + * refine `unknown` values into useful runtime types + * - Combinators such as {@link and}, {@link or}, {@link not}, and {@link xor} + * build new predicates while preserving refinement information where possible + * - {@link Tuple} and {@link Struct} lift element and property predicates to + * tuple-like arrays and object shapes + * + * **Common tasks** + * + * - Check primitive runtime types: {@link isString}, {@link isNumber}, + * {@link isBoolean}, {@link isBigInt}, {@link isSymbol} + * - Check object-like values: {@link isObject}, {@link isObjectOrArray}, + * {@link hasProperty}, {@link isTagged} + * - Combine predicates: {@link and}, {@link or}, {@link not}, {@link xor} + * - Reuse a predicate on derived input: {@link mapInput} + * - Compose refinements that narrow in stages: {@link compose} + * - Validate tuple or object shapes: {@link Tuple}, {@link Struct} + * + * **Gotchas** + * + * - Predicates only return `true` or `false`; they do not explain why a value + * failed a check + * - {@link isTruthy} uses JavaScript truthiness, so `0`, `""`, and `false` + * are rejected + * - {@link isObject} excludes arrays; use {@link isObjectOrArray} when arrays + * should also pass + * - {@link isIterable} accepts strings because strings are iterable in + * JavaScript + * - {@link isPromise} and {@link isPromiseLike} are structural checks, not + * `instanceof` checks + * - {@link isTupleOf} and {@link isTupleOfAtLeast} check length only, not + * element types + * + * **Quickstart** + * + * **Example** (Filter by a predicate) + * + * ```ts + * import { Predicate } from "effect" + * + * const values: Array = ["one", 2, "three", null] + * const strings = values.filter(Predicate.isString) + * + * console.log(strings) + * // Output: ["one", "three"] + * ``` + * + * **See also** + * + * - {@link Predicate} for plain boolean checks + * - {@link Refinement} for checks that narrow types + * - {@link Struct} and {@link Tuple} for checking compound values + * + * @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`. + * - You plan to combine checks with {@link and}/{@link or}. + * - You want a simple filter predicate for arrays or iterables. + * + * **Details** + * + * - Returns `true` or `false`; never throws by itself. + * - Does not narrow types unless you use {@link Refinement}. + * + * **Example** (Define 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. + * - You need a `TypeLambda` instance for predicate-based type classes. + * + * **Details** + * + * - Type-only; no runtime value is created. + * - 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. + * - You want to compose multiple type guards with {@link compose}. + * - You need to guard `unknown` values safely. + * + * **Details** + * + * - Returns a type predicate (`a is B`). + * - Use with `if`/`filter` to narrow types. + * + * **Example** (Narrow unknown) + * + * ```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. + * - You want to write generic helpers over predicate types. + * + * **Details** + * + * - Type-only; no runtime value is created. + * - The namespace is erased at runtime. + * + * **Example** (Extract 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. + * - You are defining generic utilities over predicates. + * + * **Details** + * + * - Type-only; no runtime value is created. + * - Resolves to `never` if the type does not match `Predicate`. + * + * **Example** (Infer input) + * + * ```ts + * import { Predicate } from "effect" + * + * type P = Predicate.Predicate + * type Input = Predicate.Predicate.In

+ * ``` + * + * @see {@link Predicate.Any} + * @see {@link Refinement.In} + * @category type-level + * @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** + * + * - Type-only; no runtime value is created. + * + * **Example** (Generic constraint) + * + * ```ts + * import { Predicate } from "effect" + * + * type AnyPredicate = Predicate.Predicate.Any + * ``` + * + * @see {@link Predicate.In} + * @category type-level + * @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/output types from refinement signatures. + * - You want to write generic helpers over refinements. + * + * **Details** + * + * - Type-only; no runtime value is created. + * - The namespace is erased at runtime. + * + * **Example** (Extract 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** + * + * - Type-only; no runtime value is created. + * - Resolves to `never` if the type does not match `Refinement`. + * + * **Example** (Infer input) + * + * ```ts + * import { Predicate } from "effect" + * + * type R = Predicate.Refinement + * type Input = Predicate.Refinement.In + * ``` + * + * @see {@link Refinement.Out} + * @see {@link Predicate.In} + * @category type-level + * @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** + * + * - Type-only; no runtime value is created. + * - Resolves to `never` if the type does not match `Refinement`. + * + * **Example** (Infer output) + * + * ```ts + * import { Predicate } from "effect" + * + * type R = Predicate.Refinement + * type Output = Predicate.Refinement.Out + * ``` + * + * @see {@link Refinement.In} + * @category type-level + * @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** + * + * - Type-only; no runtime value is created. + * + * **Example** (Generic constraint) + * + * ```ts + * import { Predicate } from "effect" + * + * type AnyRefinement = Predicate.Refinement.Any + * ``` + * + * @see {@link Refinement.In} + * @see {@link Refinement.Out} + * @category type-level + * @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 one on `B` via `B -> A`. + * - You want to check derived values (lengths, projections, etc.). + * + * **Details** + * + * - Returns a new predicate that applies `f` before `self`. + * - No short-circuit beyond what `self` does. + * + * **Example** (Check 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 runtime check for tuple length. + * - You want to narrow `ReadonlyArray` to `TupleOf`. + * + * **Details** + * + * - Only checks length, not element types. + * - Returns a refinement on the array type. + * + * **Example** (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 runtime check for tuple-like minimum length. + * - You want to narrow `ReadonlyArray` to `TupleOfAtLeast`. + * + * **Details** + * + * - Only checks length, not element types. + * - Returns a refinement on the array type. + * + * **Example** (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. + * - You need to filter out falsy values like `0`, "", and `false`. + * + * **Details** + * + * - Uses `!!input` under the hood. + * - Treats `0`, "", `false`, `null`, and `undefined` as false. + * + * **Example** (Filter truthy) + * + * ```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 runtime guard for `Set` values. + * + * **Details** + * + * - Uses `instanceof Set`. + * + * **Example** (Guard 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 runtime guard for `Map` values. + * + * **Details** + * + * - Uses `instanceof Map`. + * + * **Example** (Guard 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 to guard an `unknown` value as a string. + * - You want to narrow in `if` statements. + * + * **Details** + * + * - Uses `typeof input === "string"`. + * + * **Example** (Guard string) + * + * ```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 to guard an `unknown` value as a number. + * + * **Details** + * + * - Uses `typeof input === "number"`. + * - Does not exclude `NaN` or `Infinity`. + * + * **Example** (Guard number) + * + * ```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 to guard an `unknown` value as a boolean. + * + * **Details** + * + * - Uses `typeof input === "boolean"`. + * + * **Example** (Guard boolean) + * + * ```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 to guard an `unknown` value as a bigint. + * + * **Details** + * + * - Uses `typeof input === "bigint"`. + * + * **Example** (Guard bigint) + * + * ```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 to guard an `unknown` value as a symbol. + * + * **Details** + * + * - Uses `typeof input === "symbol"`. + * + * **Example** (Guard symbol) + * + * ```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 to guard unknown keys before indexing. + * + * **Details** + * + * - Uses {@link isString}, {@link isNumber}, and {@link isSymbol}. + * + * **Example** (Guard property key) + * + * ```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 to guard an `unknown` value as callable. + * + * **Details** + * + * - Uses `typeof input === "function"`. + * + * **Example** (Guard function) + * + * ```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 guard for optional values. + * + * **Details** + * + * - Uses `input === undefined`. + * + * **Example** (Guard undefined) + * + * ```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 want to filter out `undefined` while preserving other falsy values. + * + * **Details** + * + * - Returns a refinement that excludes `undefined`. + * + * **Example** (Filter undefined) + * + * ```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 guard for nullable values. + * + * **Details** + * + * - Uses `input === null`. + * + * **Example** (Guard null) + * + * ```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 want to filter out `null` while preserving other falsy values. + * + * **Details** + * + * - Returns a refinement that excludes `null`. + * + * **Example** (Filter null) + * + * ```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 want to guard nullish values explicitly. + * + * **Details** + * + * - Uses `input === null || input === undefined`. + * + * **Example** (Guard nullish) + * + * ```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 want to filter out nullish values but keep other falsy ones. + * + * **Details** + * + * - Uses `input != null`. + * + * **Example** (Filter non-nullish) + * + * ```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. + * + * **Details** + * + * - Always returns `false`. + * + * **Example** (Never matches) + * + * ```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. + * + * **Details** + * + * - Always returns `true`. + * + * **Example** (Always matches) + * + * ```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 want to accept plain objects and arrays, but not `null`. + * + * **Details** + * + * - Uses `typeof input === "object" && input !== null`. + * - Includes arrays. + * + * **Example** (Object or array) + * + * ```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. + * + * **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** (Guard object) + * + * ```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. + * + * **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** (Readonly object) + * + * ```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 want to accept arrays and functions as well as objects. + * + * **Details** + * + * - Returns `true` for arrays and functions, `false` for `null`. + * + * **Example** (Object keyword) + * + * ```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 to guard property access on `unknown` values. + * - You want a simple structural guard for objects. + * + * **Details** + * + * - Uses the `in` operator and {@link isObjectKeyword}. + * - Does not check property value types. + * + * **Example** (Guard property) + * + * ```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. + * - You want a quick, structural guard for tagged values. + * + * **Details** + * + * - Uses {@link hasProperty} and strict equality on `_tag`. + * + * **Example** (Guard tagged) + * + * ```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 to guard errors caught from unknown sources. + * + * **Details** + * + * - Uses `instanceof Error`. + * + * **Example** (Guard error) + * + * ```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 to guard binary data at runtime. + * + * **Details** + * + * - Uses `instanceof Uint8Array`. + * + * **Example** (Guard Uint8Array) + * + * ```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 to guard dates at runtime. + * + * **Details** + * + * - Uses `instanceof Date`. + * + * **Example** (Guard Date) + * + * ```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 guard before iterating an unknown value. + * + * **Details** + * + * - Accepts strings as iterable. + * - Uses {@link hasProperty} for `Symbol.iterator`. + * + * **Example** (Guard iterable) + * + * ```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 to detect promise instances across realms. + * + * **Details** + * + * - Structural check for `then` and `catch` functions. + * + * **Example** (Guard promise) + * + * ```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 only need `then` to interop with promise-like values. + * + * **Details** + * + * - Structural check for a callable `then`. + * + * **Example** (Guard promise-like) + * + * ```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 runtime guard for regular expressions. + * + * **Details** + * + * - Uses `instanceof RegExp`. + * + * **Example** (Guard RegExp) + * + * ```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 chain two refinements for progressive narrowing. + * - You want a predicate that applies two checks in sequence. + * + * **Details** + * + * - For refinements, the output type is narrowed by both. + * - Short-circuits on the first `false`. + * + * **Example** (Compose 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. + * - You want to lift element predicates into a tuple predicate. + * + * **Details** + * + * - Returns a refinement if any element predicate is a refinement. + * - Stops at the first failing element. + * + * **Example** (Tuple predicate) + * + * ```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. + * - You want to lift property predicates into an object predicate. + * + * **Details** + * + * - Returns a refinement if any field predicate is a refinement. + * - Checks only the specified keys; extra keys are ignored. + * + * **Example** (Struct predicate) + * + * ```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** (Negate) + * + * ```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 accept values that satisfy at least one condition. + * - You want to combine refinements with union narrowing. + * + * **Details** + * + * - Short-circuits on the first `true`. + * - For refinements, the output type is a union. + * + * **Example** (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 accept values that satisfy multiple conditions. + * - You want to combine refinements with intersection narrowing. + * + * **Details** + * + * - Short-circuits on the first `false`. + * - For refinements, the output type is an intersection. + * + * **Example** (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 an exclusive-or between two conditions. + * + * **Details** + * + * - Returns `true` when results differ. + * + * **Example** (Exclusive or) + * + * ```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 predicates. + * + * **Details** + * + * - Returns `true` when both results are equal. + * + * **Example** (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 want a rule that only applies when a precondition holds. + * - You model constraints like "if A then B". + * + * **Details** + * + * - Returns `true` when the antecedent is `false`. + * + * **Example** (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 the logical NOR of two conditions. + * + * **Details** + * + * - Returns the negation of {@link or}. + * + * **Example** (NOR) + * + * ```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 the logical NAND of two conditions. + * + * **Details** + * + * - Returns the negation of {@link and}. + * + * **Example** (NAND) + * + * ```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** + * + * - Short-circuits on the first `false`. + * - Iterates the collection each time the predicate is called. + * + * **Example** (All checks) + * + * ```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** + * + * - Short-circuits on the first `true`. + * - Iterates the collection each time the predicate is called. + * + * **Example** (Any check) + * + * ```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-smol/packages/effect/src/PrimaryKey.ts b/.repos/effect-smol/packages/effect/src/PrimaryKey.ts new file mode 100644 index 00000000000..45eeddc9db5 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/PrimaryKey.ts @@ -0,0 +1,163 @@ +/** + * 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}. + * + * **Mental model** + * + * - `PrimaryKey` is structural: no base class or registration step is required + * - The key is produced by a symbol-named method, which avoids collisions with + * ordinary object fields + * - The returned string is expected to be stable for the lifetime of the value + * when it is used for maps, caches, or persistence boundaries + * + * **Common tasks** + * + * - Implement the protocol on a class or object literal with + * `[PrimaryKey.symbol]` + * - Accept unknown input safely by checking {@link isPrimaryKey} + * - Convert a known `PrimaryKey` to its string identifier with {@link value} + * + * **Gotchas** + * + * - {@link isPrimaryKey} only checks that the symbol property exists; it does + * not call the method or verify the returned string + * - The module does not enforce global uniqueness. Choose key formats that are + * unambiguous for the domain where they are compared + * + * **Example** (Implementing a stable key) + * + * ```ts + * import { PrimaryKey } from "effect" + * + * class UserId implements PrimaryKey.PrimaryKey { + * constructor(readonly id: number) {} + * + * [PrimaryKey.symbol](): string { + * return `user:${this.id}` + * } + * } + * + * const id = new UserId(42) + * + * PrimaryKey.value(id) // "user:42" + * ``` + * + * @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-smol/packages/effect/src/PubSub.ts b/.repos/effect-smol/packages/effect/src/PubSub.ts new file mode 100644 index 00000000000..13323212cef --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/PubSub.ts @@ -0,0 +1,2835 @@ +/** + * The `PubSub` module provides asynchronous publish-subscribe hubs for + * broadcasting values to many subscribers. Publishers add messages with + * {@link publish} or {@link publishAll}; each active {@link Subscription} + * receives its own copy of every accepted message. + * + * Unlike a queue, subscribers do not compete for messages. A published value is + * retained until all subscribers that were active for that value have taken it + * or unsubscribed. + * + * **Mental model** + * + * - A `PubSub` is the shared publish side, and each `Subscription` is an + * independent read side + * - {@link subscribe} is scoped; leaving the scope automatically unsubscribes + * the subscription and releases any retained messages for it + * - {@link bounded} applies back pressure when the buffer is full, + * {@link dropping} drops new messages, and {@link sliding} drops old messages + * - {@link unbounded} removes the capacity limit but can retain an unbounded + * number of messages for slow subscribers + * - The optional replay buffer lets late subscribers first consume recently + * published messages + * + * **Common tasks** + * + * - Create hubs: {@link bounded}, {@link dropping}, {@link sliding}, + * {@link unbounded} + * - Publish values: {@link publish}, {@link publishAll} + * - Subscribe and consume: {@link subscribe}, {@link take}, {@link takeAll}, + * {@link takeUpTo}, {@link takeBetween} + * - Inspect lifecycle and capacity: {@link capacity}, {@link size}, + * {@link isFull}, {@link isEmpty}, {@link isShutdown} + * - Stop a hub: {@link shutdown}, {@link awaitShutdown} + * + * **Example** (Publishing to one scoped subscriber) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(16) + * const subscription = yield* PubSub.subscribe(pubsub) + * + * yield* PubSub.publish(pubsub, "ready") + * + * return yield* PubSub.take(subscription) + * }) + * ) + * ``` + * + * **Gotchas** + * + * - `bounded` can suspend publishers when a subscriber is slow + * - `dropping` and `sliding` can lose messages by design + * - Replay buffers are for late subscribers; they do not make the hub a + * permanent event log + * + * @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. + * + * **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 shutdown) + * + * ```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. + * + * **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 publishing from effectful code and the configured PubSub strategy + * should 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 and can handle + * `false` 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 subscription + * @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 subscription + * @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 subscription + * @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 subscription + * @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 subscription + * @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 polling from synchronous code and you can handle the `Option.none()` + * shutdown case directly. + * + * **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** (Using 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** (Using 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-smol/packages/effect/src/Pull.ts b/.repos/effect-smol/packages/effect/src/Pull.ts new file mode 100644 index 00000000000..19239e4a495 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Pull.ts @@ -0,0 +1,393 @@ +/** + * The `Pull` module provides the low-level pull-step abstraction used by + * stream-like consumers. A `Pull` is an `Effect` that can + * produce one value of type `A`, fail with an ordinary error `E`, or signal + * end-of-input with a `Cause.Done` value. + * + * **Mental model** + * + * - `Pull` is an `Effect` with a distinguished completion signal in the error channel + * - ordinary failures and completion are both represented by `Cause`, but can be separated with the helpers in this module + * - the `Done` value can carry leftover state or a final value needed by a downstream consumer + * - `Pull` is useful when repeatedly evaluating an effect until it either produces values, fails, or reports that no more input is available + * + * **Common tasks** + * + * - Extract type parameters from a pull: {@link Success}, {@link Error}, {@link Leftover}, {@link Services} + * - Detect and filter completion: {@link isDoneCause}, {@link filterDone}, {@link filterNoDone} + * - Recover from completion while preserving ordinary failures: {@link catchDone} + * - Convert done causes to successful exits: {@link doneExitFromCause} + * - Handle all outcomes explicitly: {@link matchEffect} + * + * **Gotchas** + * + * - `Cause.Done` is not an ordinary failure; use this module's helpers before treating a pull failure as an error + * - `Done` lives in the error channel, so generic `Effect` error handling can catch it unless you filter it deliberately + * - `Pull` is a low-level primitive; most user-facing stream workflows should prefer higher-level stream APIs when available + * + * @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 to test a whole pull failure cause for normal completion when you only + * need a boolean branch and do not need the done payload. + * + * @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 as a predicate when traversing `cause.reasons` and you need to identify + * done completion 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-smol/packages/effect/src/Queue.ts b/.repos/effect-smol/packages/effect/src/Queue.ts new file mode 100644 index 00000000000..809af26a072 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Queue.ts @@ -0,0 +1,1980 @@ +/** + * The `Queue` module provides asynchronous queues for communicating between + * fibers. A `Queue` accepts values of type `A`, hands each value to one + * consumer in offer order, and can later complete, fail, interrupt, or shut + * down through the queue lifecycle. + * + * **Mental model** + * + * - A queue is a fiber-aware channel with a write side ({@link Enqueue}) and a + * read side ({@link Dequeue}). + * - Producers add values with {@link offer} or {@link offerAll}; consumers + * remove values with {@link take}, {@link takeN}, {@link takeBetween}, or + * {@link takeAll}. + * - Unlike publish-subscribe hubs, consumers compete for values; a successful + * take removes the value from the queue. + * - Bounded queues use an overflow strategy: {@link bounded} suspends + * producers, {@link dropping} rejects new values, and {@link sliding} drops + * old values. + * - Operations are `Effect` values, so waiting producers and consumers compose + * with interruption, scheduling, and structured concurrency. + * + * **Common tasks** + * + * - Create queues: {@link make}, {@link bounded}, {@link dropping}, + * {@link sliding}, {@link unbounded}. + * - Restrict capabilities: {@link asEnqueue}, {@link asDequeue}. + * - Produce values: {@link offer}, {@link offerAll}. + * - Consume values: {@link take}, {@link takeN}, {@link takeBetween}, + * {@link takeAll}, {@link poll}, {@link peek}. + * - Drain or reset buffered values: {@link collect}, {@link clear}. + * - Signal lifecycle: {@link end}, {@link fail}, {@link failCause}, + * {@link interrupt}, {@link shutdown}. + * - Inspect state: {@link size}, {@link isFull}. + * + * **Example** (One producer and one consumer) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(16) + * + * yield* Queue.offer(queue, "work") + * + * return yield* Queue.take(queue) + * }) + * ``` + * + * **Gotchas** + * + * - {@link take} waits when the queue is empty; use {@link poll} when absence + * should be represented as an empty `Option`. + * - {@link dropping} and {@link sliding} queues can lose values by design; use + * {@link bounded} when every offered value must be preserved. + * - Completion and failure are observed by consumers through the queue's error + * channel, so include `Cause.Done` in the error type when using {@link end}. + * - The `Unsafe` variants are synchronous, low-level operations; prefer the + * effectful APIs in application code. + * + * **See also** + * + * - {@link Enqueue} for write-only queue handles. + * - {@link Dequeue} for read-only queue handles. + * - {@link Pull} for stream-style completion errors. + * + * @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 producers should not block and message loss is acceptable. + * Useful when you want to maintain a rolling window of the most recent messages. + * + * **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 producers should not block and existing messages should be preserved, + * but new messages may be lost 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 producers should never be blocked; unbounded queues never apply backpressure, so producers + * can always add messages successfully. This is useful when you want to prioritize + * producer throughput over memory usage control. + * + * **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. + * + * **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. + * + * **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. + * + * **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. + * + * **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. + * + * **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. + * + * **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 Size + * @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 Size + * @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. + * + * **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 Size + * @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. + * + * **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 Size + * @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-smol/packages/effect/src/Random.ts b/.repos/effect-smol/packages/effect/src/Random.ts new file mode 100644 index 00000000000..68abfb4ef04 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Random.ts @@ -0,0 +1,645 @@ +/** + * Pseudo-random generation through Effect's service context. The module exposes + * effectful generators for booleans, doubles, safe integers, bounded numbers, + * shuffling, and deterministic seeded runs. + * + * **Mental model** + * + * Randomness is read from the current {@link Random} service instead of a + * global singleton. That makes random programs reproducible in tests and local + * simulations with {@link withSeed}, while still allowing applications to + * replace the service at the edge. + * + * **Common tasks** + * + * - Draw a floating-point value in `[0, 1)` with {@link next} + * - Draw an integer with {@link nextInt} or {@link nextIntBetween} + * - Draw a floating-point value in a custom range with {@link nextBetween} + * - Randomize an iterable with {@link shuffle} + * - Run the same random sequence repeatedly with {@link withSeed} + * + * **Gotchas** + * + * - The default service is not suitable for secrets, session identifiers, + * tokens, or other security-sensitive values. + * - `withSeed` is deterministic by design. A predictable seed does not make + * generated values cryptographically secure. + * - Bounded integer generation rounds bounds before drawing from the range. + * + * **Example** (Generating random values) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function*() { + * const randomFloat = yield* Random.next + * const randomInt = yield* Random.nextInt + * const diceRoll = yield* Random.nextIntBetween(1, 6) + * + * return { randomFloat, randomInt, diceRoll } + * }) + * ``` + * + * @since 4.0.0 + */ +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 * 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 + }) + +/** + * 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) +} + +/** + * Convert a UTF-16 strings to UTF-8 encoded 32-bit integers (little-endian). + */ +function toIntArray(seed: string): Array { + let c1 = 0 // First UTF-16 code unit + let c2 = 0 // Second UTF-16 code unit (for surrogate pairs) + let unicode = 0 // Combined unicode code point from surrogate pair + const result: Array = [] // Result array of 32-bit integers + const buffer: Array = [] // Temporary buffer for the UTF-8 bytes (max 4 bytes) + const length = seed.length - 1 + + let index = 0 + while (index < length) { + c1 = seed.charCodeAt(index++) + c2 = seed.charCodeAt(index + 1) + + // 0x0000 - 0x007f: ASCII, single byte UTF-8: 0xxxxxxxx + // Example: 'A' (0x41) -> [0x41] + if (c1 < 0x0080) { + buffer.push(c1) + } // + // 0x0080 - 0x07ff: Two byte UTF-8: 110xxxxx 10xxxxxx + // Example: '¢' (0xA2) -> [0xC2, 0xA2] + else if (c1 < 0x0800) { + // First byte: upper 5 bits + 110xxxxx marker + // 0xA2 >>> 6 (0x02), & 0x1f = 0x02, | 0xc0 = 0xC2 + buffer.push(((c1 >>> 6) & 0x1f) | 0xc0) + // Second byte: lower 6 bits + 10xxxxxxxx marker + // 0xA2 & 0x3f = 0x22, | 0x80 = 0xA2 + buffer.push(((c1 >>> 0) & 0x3f) | 0x80) + } // + // 0x0800 - 0xffff (non-surrogate): Three byte UTF-8: 1110xxxx 10xxxxxx 10xxxxxx + // Example: '€' (0x20AC) -> [0xE2, 0x82, 0xAC] + else if ((c1 & 0xf800) != 0xd800) { + // First byte: top 4 bits + 1110xxxx marker + // 0x20AC >>> 12 = 0x02, & 0x0f = 0x02, | 0xe0 = 0xE2 + buffer.push(((c1 >>> 12) & 0x0f) | 0xe0) + // Second byte: middle 6 bits + 10xxxxxx marker + // 0x20AC >>> 6 = 0x82, & 0x3f = 0x02, | 0x80 = 0x82 + buffer.push(((c1 >>> 6) & 0x3f) | 0x80) + // Third byte: lower 6 bits + 10xxxxxx marker + // 0x20AC & 0x3f = 0x2C, | 0x80 = 0xAC + buffer.push(((c1 >>> 0) & 0x3f) | 0x80) + } // + // 0xd800 - 0xdfff: Surrogate pairs, four byte UTF-8: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + // Example: '𐍈' (U+10348, surrogates 0xD800 0xDF48) -> [0xF0, 0x90, 0x8D, 0x88] + else if (((c1 & 0xfc00) == 0xd800) && ((c2 & 0xfc00) == 0xdc00)) { + // Decode surrogate pair: combine 10 bits from each + 0x10000 + // ((0xDF48 & 0x3f) | ((0xD800 & 0x3f) << 10)) + 0x10000 = 0x10348 + unicode = ((c2 & 0x3f) | ((c1 & 0x3f) << 10)) + 0x10000 + // First byte: top 3 bits + 11110xxx marker + // 0x10348 >>> 18 = 0x00, & 0x07 = 0x00, | 0xf0 = 0xF0 + buffer.push(((unicode >>> 18) & 0x07) | 0xf0) + // Second byte: next 6 bits + 10xxxxxx marker + // 0x10348 >>> 12 = 0x10, & 0x3f = 0x10, | 0x80 = 0x90 + buffer.push(((unicode >>> 12) & 0x3f) | 0x80) + // Third byte: next 6 bits + 10xxxxxx marker + // 0x10348 >>> 6 = 0x40D, & 0x3f = 0x0D, | 0x80 = 0x8D + buffer.push(((unicode >>> 6) & 0x3f) | 0x80) + // Fourth byte: lower 6 bits + 10xxxxxx marker + // 0x10348 & 0x3f = 0x08, | 0x80 = 0x88 + buffer.push(((unicode >>> 0) & 0x3f) | 0x80) + index++ // Skip second surrogate + } else { + // invalid char + } + + // Pack 4 UTF-8 bytes -> 32-bit int (little-endian) + // Example: [0xE2, 0x82, 0xAC, 0x00] -> 0x00ACE2E2 + if (buffer.length > 3) { + result.push( + (buffer.shift()! << 0) | // Byte 0 at bits 0-7: 0xE2 << 0 = 0x000000E2 + (buffer.shift()! << 8) | // Byte 1 at bits 8-15: 0x82 << 8 = 0x00008200 + (buffer.shift()! << 16) | // Byte 2 at bits 16-23: 0xAC << 16 = 0x00AC0000 + (buffer.shift()! << 24) // Byte 3 at bits 24-31: 0x00 << 24 = 0x00000000 + ) + // Result: 0x00AC82E2 + } + } + + return result +} diff --git a/.repos/effect-smol/packages/effect/src/RcMap.ts b/.repos/effect-smol/packages/effect/src/RcMap.ts new file mode 100644 index 00000000000..d386c421fa3 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/RcMap.ts @@ -0,0 +1,620 @@ +/** + * The `RcMap` module provides a scoped, reference-counted map for sharing + * resources by key. It is useful when many fibers may request the same + * resource, such as a connection, client, session, or cached handle, and the + * resource should be acquired once, reused while it has active references, and + * released automatically when it is no longer needed. + * + * Each key is resolved with a user-provided lookup effect on first access via + * {@link get}. Further accesses to the same key share the in-flight or acquired + * resource and increment its reference count for the caller's current + * `Scope`. When those scopes close, references are released; resources can be + * closed immediately, kept alive for an idle time-to-live, invalidated + * explicitly, or bounded by a maximum capacity. + * + * `RcMap` is designed for Effect resource lifecycles rather than general + * mutable caching. The map itself is scoped, lookups require a `Scope`, and + * complex keys should provide `Equal` / `Hash` behavior when they need + * value-based lookup semantics. + * + * @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-smol/packages/effect/src/RcRef.ts b/.repos/effect-smol/packages/effect/src/RcRef.ts new file mode 100644 index 00000000000..62c7371b23e --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/RcRef.ts @@ -0,0 +1,259 @@ +/** + * The `RcRef` module provides a reference-counted handle for sharing one + * scoped resource across many scoped users. An `RcRef` is lazy: it + * acquires the resource the first time {@link 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. + * + * **Mental model** + * + * - {@link make} stores the acquisition effect and captures the surrounding + * `Scope` + * - {@link get} borrows the current resource for the caller's scope, + * acquiring it on demand + * - Each borrowing scope increments the reference count and installs a release + * finalizer + * - When the count reaches zero, the resource is finalized immediately or kept + * for `idleTimeToLive` + * - {@link invalidate} stops reusing the current value; active borrowers keep + * it until their scopes close + * + * **Common tasks** + * + * - Lazily share a connection, client, cache, worker, or other scoped resource + * with {@link make} + * - Borrow the shared resource inside a scoped operation with {@link get} + * - Force the next borrow to reacquire after a stale or broken resource with + * {@link invalidate} + * + * **Gotchas** + * + * - {@link get} requires `Scope`; use `Effect.scoped` or run it inside an + * existing scoped workflow + * - Invalidation does not revoke values already returned to active scopes + * - With a finite `idleTimeToLive`, a zero-reference resource remains + * available until the timeout expires + * - With an infinite `idleTimeToLive`, an idle resource remains until + * invalidated or the owning scope closes + * + * @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`. + * + * **When to use** + * + * Use to reference 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-smol/packages/effect/src/Record.ts b/.repos/effect-smol/packages/effect/src/Record.ts new file mode 100644 index 00000000000..4e4e96aef83 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Record.ts @@ -0,0 +1,1628 @@ +/** + * Tools for working with plain JavaScript records as immutable key-value + * dictionaries. The module covers construction, lookup, updates, mapping, + * filtering, folding, set-like combination, and typed conversions between + * records and iterable entries. + * + * Reach for `Record` when your data is already represented as a plain object + * with string or symbol keys and you want typed, data-first or data-last + * helpers that return new records instead of mutating the input. + * + * **Mental model** + * + * - A `ReadonlyRecord` is a plain object whose keys are known by type and + * whose values share a common type. + * - Operations such as {@link set}, {@link remove}, {@link map}, and + * {@link filter} allocate new plain objects. + * - Lookups and updates that might miss a key return `Option` values through + * APIs such as {@link get}, {@link modify}, {@link replace}, and {@link pop}. + * - Many APIs are dual, so they work both as `Record.map(record, f)` and in + * pipelines as `pipe(record, Record.map(f))`. + * + * **Common tasks** + * + * - Create records: {@link empty}, {@link singleton}, {@link fromEntries}, + * {@link fromIterableBy}, {@link fromIterableWith} + * - Inspect records: {@link isEmptyRecord}, {@link size}, {@link has}, + * {@link get}, {@link keys}, {@link values}, {@link toEntries} + * - Add, update, or remove entries: {@link set}, {@link modify}, + * {@link replace}, {@link remove}, {@link pop} + * - Transform entries: {@link map}, {@link mapKeys}, {@link mapEntries}, + * {@link collect} + * - Filter or partition: {@link filter}, {@link filterMap}, {@link getSomes}, + * {@link getFailures}, {@link getSuccesses}, {@link partition}, + * {@link separate} + * - Fold and search: {@link reduce}, {@link every}, {@link some}, + * {@link findFirst} + * - Combine records: {@link union}, {@link intersection}, {@link difference}, + * {@link makeReducerUnion}, {@link makeReducerIntersection} + * - Compare records: {@link isSubrecord}, {@link isSubrecordBy}, + * {@link makeEquivalence} + * + * **Gotchas** + * + * - Iteration-based APIs use `Object.keys`, so they visit enumerable string + * keys. Targeted APIs such as {@link has}, {@link get}, {@link set}, and + * {@link remove} can work with symbol keys, but {@link keys}, {@link values}, + * {@link map}, and similar traversal APIs do not visit symbols. + * - When duplicate keys are produced by constructors or key-mapping APIs, later + * writes overwrite earlier values according to normal object assignment. + * + * **Quickstart** + * + * **Example** (Transforming a record without mutation) + * + * ```ts + * import { Record } from "effect" + * + * const scores = { alice: 1, bob: 2 } + * + * const next = Record.set(scores, "carol", 3) + * const doubled = Record.map(next, (score) => score * 2) + * + * console.log(scores) // { alice: 1, bob: 2 } + * console.log(doubled) // { alice: 2, bob: 4, carol: 6 } + * console.log(Record.get(doubled, "alice")) // Option.some(2) + * ``` + * + * **See also** + * + * - `Struct` for fixed-shape objects where each field may have a + * different type + * - `HashMap` for immutable maps with arbitrary key types and Effect + * equality / hashing semantics + * + * @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 utils + * @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 utils + * @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 utils + * @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 utils + * @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 utils + * @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-smol/packages/effect/src/Redactable.ts b/.repos/effect-smol/packages/effect/src/Redactable.ts new file mode 100644 index 00000000000..89759683887 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Redactable.ts @@ -0,0 +1,226 @@ +/** + * 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. + * + * ## Mental model + * + * - **Redactable** - an object that implements `[symbolRedactable]`, a method + * that receives the current `Context` and returns a replacement value. + * - **symbolRedactable** - the well-known `Symbol` key that marks an object as + * redactable. + * - **redact** - the primary entry point: pass any value and get back either its + * redacted form (if it is `Redactable`) or the original value unchanged. + * - **getRedacted** - lower-level helper that calls `[symbolRedactable]` directly + * on a value already known to be `Redactable`. + * - The `Context` passed to `[symbolRedactable]` comes from the current fiber. + * If no fiber is active, an empty `Context` is used. + * + * ## Common tasks + * + * - **Make a value redactable**: implement the {@link Redactable} interface by + * adding a `[symbolRedactable]` method. + * - **Redact an unknown value**: call {@link redact} - it returns the original + * value when it is not redactable. + * - **Check if a value is redactable**: use {@link isRedactable}. + * - **Get the redacted form of a known `Redactable`**: use {@link getRedacted}. + * + * ## Gotchas + * + * - `[symbolRedactable]` receives the fiber's `Context` as its argument. + * - Outside of an Effect runtime (no current fiber), `getRedacted` still works + * but passes an empty `Context`, so service lookups will not find anything. + * - `redact` is not recursive: if a redactable object contains nested + * redactable values, only the outermost redaction is applied. + * + * ## Quickstart + * + * **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) + "..." + * } + * } + * + * const key = new ApiKey("sk-1234567890abcdef") + * + * console.log(Redactable.isRedactable(key)) // true + * console.log(Redactable.redact(key)) // "sk-1..." + * console.log(Redactable.redact("plain")) // "plain" + * ``` + * + * ## See also + * + * - {@link Redactable} - the interface to implement + * - {@link symbolRedactable} - the symbol key + * - {@link redact} - the main redaction entry point + * + * @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 have already verified the value is `Redactable`, for + * example with {@link isRedactable}, and want to avoid a second check. + * + * **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-smol/packages/effect/src/Redacted.ts b/.repos/effect-smol/packages/effect/src/Redacted.ts new file mode 100644 index 00000000000..b58a30f959e --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Redacted.ts @@ -0,0 +1,347 @@ +/** + * The `Redacted` module wraps sensitive values so normal rendering paths show + * a placeholder instead of the underlying value. It is designed to reduce + * accidental disclosure in logs, error messages, JSON output, and inspection + * output while still allowing trusted code to recover the original value. + * + * **Mental model** + * + * - A `Redacted` carries an underlying value of type `A` behind a wrapper + * - String, JSON, and inspection output render as `` or + * `` when a label is provided + * - {@link value} retrieves the underlying value and should be used only at + * trusted boundaries + * - Equality and hashing are based on the underlying value, not the redacted + * placeholder + * + * **Common tasks** + * + * - Wrap a secret with {@link make} + * - Check unknown input with {@link isRedacted} + * - Recover the secret at a trusted boundary with {@link value} + * - Remove the stored association with {@link wipeUnsafe} + * - Compare redacted values with an underlying equivalence via + * {@link makeEquivalence} + * + * **Gotchas** + * + * - `Redacted` is not encryption and does not zero memory; it reduces + * accidental display of sensitive values + * - Labels are visible in rendered output, so labels must not contain secrets + * - After {@link wipeUnsafe}, calling {@link value} on the same wrapper fails + * + * **Example** (Rendering a redacted value) + * + * ```ts + * import { Redacted } from "effect" + * + * const token = Redacted.make("secret-token", { label: "api-token" }) + * + * String(token) // "" + * Redacted.value(token) // "secret-token" + * ``` + * + * @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 type-level + * @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 the underlying sensitive value is required 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 an API needs an `Equivalence` for `Redacted` values based on their + * underlying values. + * + * **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 equivalence + * @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-smol/packages/effect/src/Reducer.ts b/.repos/effect-smol/packages/effect/src/Reducer.ts new file mode 100644 index 00000000000..1cade8a001c --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Reducer.ts @@ -0,0 +1,238 @@ +/** + * A module for reducing collections of values into a single result. + * + * A `Reducer` extends {@link Combiner.Combiner} by adding an + * `initialValue` (identity element) and a `combineAll` method that folds an + * entire collection. Think `Array.prototype.reduce`, but packaged as a + * reusable, composable value. + * + * ## Mental model + * + * - **Reducer** – a {@link Combiner.Combiner} plus an `initialValue` and a + * `combineAll` method. + * - **initialValue** – the neutral/identity element. Combining any value with + * `initialValue` should return the original value unchanged (e.g. `0` for + * addition, `""` for string concatenation). + * - **combineAll** – folds an `Iterable` starting from `initialValue`. + * When omitted from {@link make}, a default left-to-right fold is used. + * - **Purity** – all reducers produced by this module are pure; they never + * mutate their arguments. + * - **Composability** – reducers can be lifted into `Option`, `Struct`, + * `Tuple`, `Record`, and other container types via helpers in those modules. + * - **Subtype of Combiner** – every `Reducer` is also a valid + * `Combiner`, so you can pass a `Reducer` anywhere a `Combiner` is + * expected. + * + * ## Common tasks + * + * - Create a reducer from a combine function and initial value → {@link make} + * - Swap argument order → {@link flip} + * - Combine two values without an initial value → use {@link Combiner.Combiner} + * instead + * + * ## Gotchas + * + * - `combineAll` on an empty iterable returns `initialValue`, not an error. + * - The default `combineAll` folds left-to-right. If your `combine` is not + * associative, order matters. Pass a custom `combineAll` to {@link make} if + * you need different traversal or short-circuiting. + * - A `Reducer` is also a valid `Combiner` — but a `Combiner` is *not* a + * `Reducer` (it lacks `initialValue`). + * + * ## Quickstart + * + * **Example** (summing a list of numbers) + * + * ```ts + * import { Reducer } from "effect" + * + * const Sum = Reducer.make((a, b) => a + b, 0) + * + * console.log(Sum.combine(3, 4)) + * // Output: 7 + * + * console.log(Sum.combineAll([1, 2, 3, 4])) + * // Output: 10 + * + * console.log(Sum.combineAll([])) + * // Output: 0 + * ``` + * + * ## See also + * + * - {@link make} – the primary constructor + * - {@link Reducer} – the core interface + * - {@link Combiner.Combiner} – the parent interface (no `initialValue`) + * + * @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** (Multiplication 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 need the "right" value to act as the accumulator side. + * - You want to reverse the natural direction of a non-commutative reducer + * (e.g. string concatenation becomes prepend). + * + * **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-smol/packages/effect/src/Ref.ts b/.repos/effect-smol/packages/effect/src/Ref.ts new file mode 100644 index 00000000000..2cc4c4fb60f --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Ref.ts @@ -0,0 +1,841 @@ +/** + * The `Ref` module provides fiber-safe mutable references for state inside + * Effect programs. A `Ref` holds one value of type `A` and exposes reads, + * writes, and atomic transformations as effects, so state changes compose with + * Effect's concurrency model. + * + * **Mental model** + * + * - {@link make} creates a reference with an initial value + * - {@link get} reads the current value, and {@link set} replaces it + * - {@link update}, {@link updateAndGet}, and {@link getAndUpdate} transform + * the current value atomically + * - {@link modify} updates the value and returns a separate result computed + * from the previous value + * - The `Some` variants use `Option` to leave the value unchanged when a + * partial update does not apply + * + * **Common tasks** + * + * - Create refs: {@link make}, {@link makeUnsafe} + * - Read and write: {@link get}, {@link set}, {@link getAndSet}, + * {@link setAndGet} + * - Update state: {@link update}, {@link updateAndGet}, {@link getAndUpdate} + * - Update conditionally: {@link updateSome}, {@link updateSomeAndGet}, + * {@link getAndUpdateSome}, {@link modifySome} + * - Compute while updating: {@link modify} + * + * **Example** (Updating shared state) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* Ref.make(0) + * + * const next = yield* Ref.updateAndGet(counter, (n) => n + 1) + * const label = yield* Ref.modify(counter, (n) => [`count=${n}`, n + 1]) + * + * return { label, next } + * }) + * ``` + * + * **Gotchas** + * + * - Each `Ref` operation is atomic for that reference, but multiple refs are + * not updated transactionally as a group + * - If the new value depends on the old value, prefer {@link update} or + * {@link modify} over a separate {@link get} followed by {@link set} + * - Unsafe operations are synchronous low-level accessors; prefer effectful + * operations in application code + * + * @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 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 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 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 the 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 utils + * @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 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 utils + * @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 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 utils + * @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 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 utils + * @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 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 the stored value. + * + * **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 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 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 utils + * @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 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 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 utils + * @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-smol/packages/effect/src/References.ts b/.repos/effect-smol/packages/effect/src/References.ts new file mode 100644 index 00000000000..ad47523829a --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/References.ts @@ -0,0 +1,835 @@ +/** + * 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. + * + * **Mental model** + * + * - Each exported value is a context key with a runtime default, so it can be + * read even when application code has not provided an override. + * - Runtime services such as logging, tracing, scheduling, and concurrency + * controls read these keys from the current context. + * - `Effect.provideService` overrides a reference for a specific effect scope; + * it does not mutate a process-wide global. + * - Child fibers inherit the context active at the point they are started. + * + * **Common tasks** + * + * - Limit default concurrency with {@link CurrentConcurrency}. + * - Control log severity and filtering with {@link CurrentLogLevel}, + * {@link MinimumLogLevel}, and {@link UnhandledLogLevel}. + * - Attach log metadata with {@link CurrentLogAnnotations} and + * {@link CurrentLogSpans}, or replace the active loggers with + * {@link CurrentLoggers}. + * - Configure tracing with {@link Tracer}, {@link TracerEnabled}, + * {@link TracerTimingEnabled}, {@link TracerSpanAnnotations}, and + * {@link TracerSpanLinks}. + * - Tune scheduler behavior with {@link Scheduler}, {@link MaxOpsBeforeYield}, + * and {@link PreventSchedulerYield}. + * + * **Example** (Providing a runtime reference) + * + * ```ts + * import { Effect, References } from "effect" + * + * const readConcurrency = Effect.gen(function*() { + * return yield* References.CurrentConcurrency + * }) + * + * const limited = Effect.provideService( + * readConcurrency, + * References.CurrentConcurrency, + * 4 + * ) + * ``` + * + * **Gotchas** + * + * - Prefer higher-level logging, tracing, and concurrency APIs when they fit; + * this module is for direct access to the runtime knobs they use. + * - `CurrentLogLevel` chooses the severity assigned to ordinary log entries, + * while `MinimumLogLevel` decides which entries are filtered out. + * - References are context values, so scoped overrides end when the provided + * effect completes. + * + * @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 tune scheduler fairness for CPU-bound fibers by changing the + * 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 keep stdout reserved for protocol messages or data output while still + * allowing Effect runtime logs to be emitted. + * + * **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-smol/packages/effect/src/RegExp.ts b/.repos/effect-smol/packages/effect/src/RegExp.ts new file mode 100644 index 00000000000..9b7fa0a6f1a --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/RegExp.ts @@ -0,0 +1,108 @@ +/** + * 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. + * + * **Common tasks** + * + * - Construct expressions with the native constructor: {@link RegExp} + * - Narrow unknown input to `RegExp`: {@link isRegExp} + * - Escape literal text before interpolating it into a pattern: {@link escape} + * + * **Gotchas** + * + * - {@link escape} only escapes regular expression metacharacters in a string. + * It does not add anchors, flags, grouping, or validation for a full pattern. + * + * **Quickstart** + * + * **Example** (Matching literal text) + * + * ```ts + * import { RegExp } from "effect" + * + * const literal = "a+b.txt" + * const expression = new RegExp.RegExp(`^${RegExp.escape(literal)}$`) + * + * console.log(expression.test("a+b.txt")) // true + * console.log(expression.test("aaab.txt")) // false + * console.log(RegExp.isRegExp(expression)) // true + * ``` + * + * @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 utils + * @since 2.0.0 + */ +export const escape = (string: string): string => string.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&") diff --git a/.repos/effect-smol/packages/effect/src/Request.ts b/.repos/effect-smol/packages/effect/src/Request.ts new file mode 100644 index 00000000000..81596d042b2 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Request.ts @@ -0,0 +1,633 @@ +/** + * The `Request` module defines typed request values for data loading with + * `Effect.request`. A request is a description of work, not the execution of + * that work: it records the success type, typed error, service requirements, + * and the fields needed by a resolver to perform one logical operation. + * + * Requests are designed to be paired with a `RequestResolver`, which receives + * pending request entries, batches or caches them when possible, and completes + * each entry with a result. This lets calling code ask for data declaratively + * while resolver code owns the backend-specific loading logic. + * + * **Mental model** + * + * - `Request` describes one operation that succeeds with `A`, fails + * with `E`, and may require services `R` + * - Constructors such as {@link of}, {@link tagged}, {@link Class}, and + * {@link TaggedClass} attach the request marker and structural behavior + * expected by the request runtime + * - A resolver receives {@link Entry} values; each entry contains the original + * request, the captured context, and a completion callback + * - Completion helpers such as {@link succeed}, {@link fail}, + * {@link failCause}, {@link complete}, and {@link completeEffect} turn + * resolver results into the `Exit` expected by the waiting fiber + * + * **Common tasks** + * + * - Define request shapes with {@link Request}, {@link Class}, or + * {@link TaggedClass} + * - Build lightweight request constructors with {@link of} or {@link tagged} + * - Check unknown values with {@link isRequest} + * - Complete pending resolver entries with {@link succeed}, {@link fail}, + * {@link failCause}, {@link complete}, or {@link completeEffect} + * - Extract request type members with {@link Success}, {@link Error}, + * {@link Services}, and {@link Result} + * + * **Gotchas** + * + * - Creating a request value does not run anything; it must be submitted with + * `Effect.request` and handled by a resolver + * - Resolver implementations must complete every {@link Entry} they receive, + * otherwise the fiber waiting for that request will not receive a value + * - Cached and deduplicated requests depend on request identity and structural + * equality, so include only stable fields that describe the logical operation + * + * @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 type-level + * @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 type-level + * @since 2.0.0 + */ +export type Success> = [T] extends [Request] ? _A + : never + +/** + * A utility type to extract the requirements type from a `Request`. + * + * @category type-level + * @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 type-level + * @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 to finish a `Request.Entry` when you already have the request's 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 a `RequestResolver` needs 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 to finish a `Request.Entry` when you have a successful value for the + * request. + * + * @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-smol/packages/effect/src/RequestResolver.ts b/.repos/effect-smol/packages/effect/src/RequestResolver.ts new file mode 100644 index 00000000000..f04efcf1835 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/RequestResolver.ts @@ -0,0 +1,1284 @@ +/** + * The `RequestResolver` module provides the data-loading side of + * `Effect.request`. A `Request` describes what a fiber needs, while a + * `RequestResolver` describes how to collect, batch, execute, cache, trace, + * and complete those requests. + * + * **Mental model** + * + * - A resolver receives one or more `Request.Entry` values and must complete + * each entry with either a success or failure + * - Concurrent requests made with the same resolver can be gathered into a + * batch before the resolver is run + * - Batch keys split pending requests into independent groups, which is useful + * when different backends, tenants, or query shapes must be resolved + * separately + * - Delays and `batchN` tune how long requests are collected and how large + * each batch may become + * - Resolvers can be wrapped with tracing, in-memory caching, cache services, + * and persistence without changing the request type + * + * **Common tasks** + * + * - Create a resolver from batch logic: {@link make} + * - Create grouped batch logic: {@link makeGrouped} or {@link grouped} + * - Create a resolver from pure logic: {@link fromFunction} or + * {@link fromFunctionBatched} + * - Create a resolver from effectful logic: {@link fromEffect} or + * {@link fromEffectTagged} + * - Control batching: {@link setDelay}, {@link setDelayEffect}, + * {@link batchN} + * - Add operational behavior: {@link around}, {@link race}, {@link withSpan} + * - Reuse results: {@link withCache}, {@link asCache}, {@link persisted} + * + * **Gotchas** + * + * - Every entry passed to a resolver must be completed; leaving an entry + * incomplete causes the waiting request to fail + * - Batched result collections must line up with the input entries in order + * and length when using the batched helper constructors + * - Grouping controls which requests share a resolver run; choose stable keys + * for requests that can safely be handled together + * - Caching and persistence depend on request identity and the request's + * equality semantics, so model request values deliberately when cached + * + * @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-smol/packages/effect/src/Resource.ts b/.repos/effect-smol/packages/effect/src/Resource.ts new file mode 100644 index 00000000000..cb07fafbd7b --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Resource.ts @@ -0,0 +1,209 @@ +/** + * The `Resource` module provides refreshable, scoped values. A + * `Resource` stores the latest successful or failed acquisition result and + * can be read with {@link get}, refreshed manually with {@link refresh}, or + * refreshed automatically with {@link auto}. + * + * **Mental model** + * + * - A `Resource` wraps an acquisition `Effect` whose result is kept in a + * `ScopedRef` + * - Each refresh re-runs acquisition and replaces the stored `Exit` + * - Replacing the stored value releases resources associated with the previous + * scoped value + * - Reading a resource returns the current acquired value or fails with the + * current acquisition error + * + * **Common tasks** + * + * - Create a manually refreshed resource with {@link manual} + * - Create a schedule-driven resource with {@link auto} + * - Read the current value with {@link get} + * - Force a reload with {@link refresh} + * - Check whether an unknown value is a resource with {@link isResource} + * + * **Gotchas** + * + * - Creating a resource requires a `Scope`; when the scope closes, scoped + * values held by the resource are released + * - Failed acquisitions are stored too, so subsequent {@link get} calls fail + * until a refresh succeeds + * - Automatic refreshes run in the resource scope and stop when that scope is + * closed + * + * @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 to control the timing of resource refreshes yourself rather + * than relying on 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 utils + * @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-smol/packages/effect/src/Result.ts b/.repos/effect-smol/packages/effect/src/Result.ts new file mode 100644 index 00000000000..b5f65dc4c4f --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Result.ts @@ -0,0 +1,1969 @@ +/** + * A synchronous, pure type for representing computations that can succeed + * (`Success`) or fail (`Failure`). Unlike `Effect`, `Result` is + * evaluated eagerly and carries no side effects. + * + * **Mental model** + * + * - `Result` is a discriminated union: `Success | Failure` + * - `Success` wraps a value of type `A`, accessed via `.success` + * - `Failure` wraps an error of type `E`, accessed via `.failure` + * - `Result` is a monad: chain operations with {@link flatMap}, compose pipelines with `pipe` + * - All operations are pure and return new `Result` values; the input is never mutated + * - `Result` is yieldable in `Effect.gen`, producing the inner value or short-circuiting on failure + * + * **Common tasks** + * + * - Create from a value: {@link succeed}, {@link fail} + * - Create from nullable: {@link fromNullishOr} + * - Create from Option: {@link fromOption} + * - Create from throwing code: {@link try_ try} + * - Create from predicate: {@link liftPredicate} + * - Transform: {@link map}, {@link mapError}, {@link mapBoth} + * - Unwrap: {@link getOrElse}, {@link getOrNull}, {@link getOrUndefined}, {@link getOrThrow} + * - Pattern match: {@link match} + * - Sequence: {@link flatMap}, {@link andThen}, {@link all} + * - Recover: {@link orElse} + * - Filter: {@link filterOrFail} + * - Convert to Option: {@link getSuccess}, {@link getFailure} + * - Generator syntax: {@link gen} + * - Do notation: {@link Do}, {@link bind}, {@link let_ let} + * - Check variant: {@link isResult}, {@link isSuccess}, {@link isFailure} + * + * **Gotchas** + * + * - `E` defaults to `never`, so `Result` means a result that cannot fail + * - {@link andThen} accepts a `Result`, a function returning a `Result`, a plain value, or a function returning a plain value; {@link flatMap} only accepts a function returning a `Result` + * - {@link all} short-circuits on the first `Failure` and returns it; later elements are not inspected + * - {@link getOrThrow} throws the raw failure value `E`; use {@link getOrThrowWith} for custom error objects + * - {@link tap} runs a side-effect but does not change the result; its return value is ignored + * + * **Quickstart** + * + * **Example** (Parsing and validating with Result) + * + * ```ts + * import { Result } from "effect" + * + * const parse = (input: string): Result.Result => + * isNaN(Number(input)) + * ? Result.fail("not a number") + * : Result.succeed(Number(input)) + * + * const ensurePositive = (n: number): Result.Result => + * n > 0 ? Result.succeed(n) : Result.fail("not positive") + * + * const result = Result.flatMap(parse("42"), ensurePositive) + * + * console.log(Result.getOrElse(result, (err) => `Error: ${err}`)) + * // Output: 42 + * ``` + * + * **See also** + * + * - {@link succeed} / {@link fail} to create values + * - {@link match} to fold both branches + * - {@link gen} for generator-based composition + * + * @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`. + * + * **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 computation 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 a success should signal completion without carrying a meaningful + * value. + * + * **Details** + * + * This is equivalent to `Result.succeed(undefined)`, but reuses a shared + * `Success` wrapper instead of allocating one each time. + * + * **Example** (Using void result) + * + * ```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 a failure should act only as a control signal and no failure value + * is needed. + * + * **Details** + * + * This is equivalent to `Result.fail(undefined)` with type + * `Result`, but reuses a shared `Failure` wrapper instead of + * allocating one each time. + * + * **Example** (Using a failure 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 an input may be `null` or `undefined` and absence should be + * represented as a `Failure` while present values should 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 Type 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 Type 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 Type 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 to extract only the success value 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 to extract only the failure value 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 Equivalence + * @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. + * + * **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) + * - Use when you need to "exit" the `Result` type and produce a plain value + * + * **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 with 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 with 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`. + * + * **Details** + * + * - `Success` is returned unchanged + * - `Failure` calls `that(e)` to produce a new `Result` + * - Use to provide a recovery path or fallback computation on failure + * + * **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** (Sequential validation) + * + * ```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** (Using andThen 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 utils + * @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 sequential `Result` composition is clearer with generator syntax + * than 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 adding a `Result`-producing step to a do-notation pipeline and + * storing 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 to add a pure computed field to a do-notation accumulator. + * + * **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>`. + * + * **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>`. + * + * **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** (Map and transpose 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** (Using succeedNone) + * + * ```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-smol/packages/effect/src/Runtime.ts b/.repos/effect-smol/packages/effect/src/Runtime.ts new file mode 100644 index 00000000000..50990f2f164 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Runtime.ts @@ -0,0 +1,477 @@ +/** + * Runtime 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. Application code + * usually calls the platform-provided runner; runtime integrations use + * {@link makeRunMain} directly. + * + * **Mental model** + * + * - {@link makeRunMain} forks the supplied `Effect` and gives the host adapter + * the running fiber plus a teardown callback + * - The host adapter installs platform-specific hooks such as signal handlers, + * fiber observers, process exits, worker termination, or test harness + * callbacks + * - {@link defaultTeardown} maps successful exits to `0`, interruption-only + * failures to `130`, failures with {@link errorExitCode} to that code, and + * other failures to `1` + * - {@link errorReported} controls automatic failure logging; set it to + * `false` on errors that have already been reported + * + * **Common tasks** + * + * - Build a platform runner: {@link makeRunMain} + * - Reuse the standard exit-code rules: {@link defaultTeardown} + * - Customize failure exit codes: {@link errorExitCode}, + * {@link getErrorExitCode} + * - Control automatic failure logging: {@link errorReported}, + * {@link getErrorReported} + * + * **Gotchas** + * + * - `makeRunMain` starts the main fiber, but the adapter callback is + * responsible for observing that fiber and eventually invoking teardown. + * - `disableErrorReporting` only disables automatic failure logging. It does + * not change the `Exit`, interruption behavior, or teardown exit-code rules. + * - Error markers are read from `Cause.squash(cause)`, so causes with multiple + * failures use the squashed failure value to determine logging and exit code. + * + * **Example** (Creating a minimal runner) + * + * ```ts + * import { Effect, Runtime } from "effect" + * + * const runMain = Runtime.makeRunMain(({ fiber, teardown }) => { + * fiber.addObserver((exit) => { + * teardown(exit, (code) => { + * console.log(`finished with exit code ${code}`) + * }) + * }) + * }) + * + * runMain(Effect.log("booted")) + * ``` + * + * @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 when you want 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** (Using 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. Most applications + * should use a platform-provided runner, such as `NodeRuntime.runMain`, rather + * than constructing one directly. + * + * **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. + * + * `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 error classes are already reported by application code and should + * not be 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-smol/packages/effect/src/Schedule.ts b/.repos/effect-smol/packages/effect/src/Schedule.ts new file mode 100644 index 00000000000..4d43a5b76f1 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Schedule.ts @@ -0,0 +1,3608 @@ +/** + * Declarative policies for retrying, repeating, and pacing Effect programs. + * + * A `Schedule` is a stateful policy that is stepped + * with an input value. Each step either stops the recurrence or emits an output + * together with the delay before the next step. Schedules are values: they can + * be transformed, combined, and passed to retry, repeat, stream, and channel + * APIs without running anything until the surrounding Effect program runs. + * + * **Mental model** + * + * - Constructors such as {@link recurs}, {@link spaced}, {@link fixed}, + * {@link exponential}, and {@link fibonacci} define the base cadence or + * stopping rule. + * - Combinators such as {@link both}, {@link either}, and {@link andThen} + * combine schedules and determine both stopping behavior and output shape. + * - Delay helpers such as {@link addDelay}, {@link modifyDelay}, and + * {@link jittered} adjust pacing while preserving the schedule's control + * flow. + * - Metadata-aware constructors and {@link CurrentMetadata} expose attempts, + * elapsed time, input, output, and duration for advanced policies. + * + * **Common tasks** + * + * - Retry a failing effect with exponential backoff and a maximum retry count. + * - Repeat a successful effect on a fixed or spaced interval. + * - Add jitter so concurrent clients do not retry at the same instant. + * - Transform or retain schedule outputs with {@link map}, + * {@link collectOutputs}, and {@link take}. + * + * **Example** (Retrying with bounded exponential backoff) + * + * ```ts + * import { Effect, Schedule } from "effect" + * + * let attempts = 0 + * + * const request = Effect.sync(() => { + * attempts += 1 + * return attempts + * }).pipe( + * Effect.flatMap((attempt) => + * attempt < 3 ? Effect.fail("temporary failure") : Effect.succeed("ok") + * ) + * ) + * + * const policy = Schedule.exponential("100 millis").pipe( + * Schedule.jittered, + * Schedule.both(Schedule.recurs(5)) + * ) + * + * const program = Effect.retry(request, policy) + * ``` + * + * **Gotchas** + * + * - A schedule is a description; delays happen only when an operation such as + * `Effect.retry` or `Effect.repeat` runs it. + * - `Schedule.recurs(3)` allows three recurrences in addition to the first run + * performed by retry or repeat. + * - Combining schedules changes the output type as well as the stopping rule, + * so check the resulting type when using {@link both}, {@link either}, or + * {@link andThen}. + * + * @since 2.0.0 + */ +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 type { LazyArg } from "./Function.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 } 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.exponential("100 millis").pipe( + * Schedule.both(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.take(5))) + * ) + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Schedule + extends Schedule.Variance, Pipeable +{} + +/** + * Metadata provided to schedule functions containing timing and input information. + * + * **Example** (Reading schedule input metadata) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Custom schedule that uses input metadata + * const metadataAwareSchedule = Schedule.spaced("1 second").pipe( + * Schedule.collectWhile((metadata) => + * Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 10000) + * ) + * ) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Console.log("Task execution"), + * metadataAwareSchedule + * ) + * }) + * ``` + * + * @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. + * + * **Example** (Logging schedule output metadata) + * + * ```ts + * import { Console, Duration, Effect, Schedule } from "effect" + * + * // Custom schedule that logs metadata and output for each recurrence + * const loggingSchedule = Schedule.unfold(0, (n) => Effect.succeed(n + 1)).pipe( + * Schedule.addDelay(() => Effect.succeed(Duration.millis(100))), + * Schedule.collectWhile((metadata) => + * Console.log( + * `Output: ${metadata.output}, attempt: ${metadata.attempt}, elapsed: ${metadata.elapsed}ms` + * ).pipe(Effect.as(metadata.attempt <= 3)) + * ) + * ) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.succeed("task completed"), + * loggingSchedule.pipe(Schedule.take(3)) + * ) + * }) + * + * // Output logs will show: + * // Output: 0, attempt: 1, elapsed: 0ms + * // Output: 1, attempt: 2, elapsed: 100ms + * // Output: 2, attempt: 3, elapsed: 200ms + * ``` + * + * @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. + * + * **Example** (Creating custom schedules with the namespace) + * + * ```ts + * import { Duration, Effect, Schedule } from "effect" + * + * // Usage of the Schedule namespace for creating schedules + * + * // Create custom schedule with metadata + * const customSchedule = Schedule.unfold(0, (n) => Effect.succeed(n + 1)).pipe( + * Schedule.addDelay((n) => Effect.succeed(Duration.millis(n * 100))) + * ) + * + * const program = Effect.gen(function*() { + * let attempt = 0 + * + * yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * if (attempt < 3) { + * return yield* Effect.fail(`Attempt ${attempt} failed`) + * } + * return `Success on attempt ${attempt}` + * }), + * customSchedule.pipe(Schedule.take(5)) + * ) + * }) + * ``` + * + * @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.take(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 + } +} + +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.take(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.take(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 output + * const delayedSchedule = Schedule.addDelay( + * Schedule.exponential("100 millis").pipe(Schedule.take(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.tapOutput((delay) => + * Console.log(`Base delay: ${delay}`) + * ) + * ) + * ) + * }) + * + * // Add adaptive delay based on execution count + * const adaptiveSchedule = Schedule.addDelay( + * Schedule.recurs(6), + * (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.tapOutput((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.take(4)), + * (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.take(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 utils + * @since 2.0.0 + */ +export const addDelay: { + ( + f: (output: Output) => Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + f: (output: Output) => Effect + ): Schedule +} = dual(2, ( + self: Schedule, + f: (output: Output) => Effect +): Schedule => + modifyDelay( + self, + (output, delay) => + effect.map(f(output), (d) => Duration.sum(Duration.fromInputUnsafe(d), Duration.fromInputUnsafe(delay))) + )) + +/** + * 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.take(3) + * ) + * const slowRetries = Schedule.exponential("1 second").pipe( + * Schedule.take(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), (result) => effect.succeed(Result.merge(result)))) + +/** + * 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.take(2)), + * Schedule.spaced("500 millis").pipe(Schedule.take(2)) + * ) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task-result" + * }), + * phaseTracker.pipe( + * Schedule.tapOutput((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, Result.succeed) + const right = map(other, Result.fail) + 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 two `Schedule`s by recurring if both of the two schedules want + * to recur, using the maximum of the two durations between recurrences and + * outputting a tuple of the outputs of both schedules. + * + * **When to use** + * + * Use when the combined schedule should continue only while both + * schedules still recur. Use `either` when either schedule should be enough to + * continue. + * + * **Example** (Combining time and attempt limits) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * // Both schedules must want to continue for the combined schedule to continue + * const timeLimit = Schedule.spaced("1 second").pipe(Schedule.take(5)) // max 5 times + * const attemptLimit = Schedule.recurs(3) // max 3 attempts + * + * // Continues only while BOTH schedules want to continue (intersection/AND logic) + * const bothSchedule = Schedule.both(timeLimit, attemptLimit) + * // Outputs: [time_result, attempt_count] tuple + * + * const program = Effect.gen(function*() { + * const results = yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task completed" + * }), + * bothSchedule.pipe( + * Schedule.tapOutput(([timeResult, attemptResult]) => + * Console.log(`Time: ${timeResult}, Attempts: ${attemptResult}`) + * ) + * ) + * ) + * + * yield* Console.log("Completed all executions") + * }) + * + * // Both with different delay strategies - uses maximum delay + * const fastSchedule = Schedule.fixed("500 millis").pipe(Schedule.take(4)) + * const slowSchedule = Schedule.spaced("2 seconds").pipe(Schedule.take(6)) + * + * // Will use the slower (maximum) delay and stop when first schedule exhausts + * const conservativeSchedule = Schedule.both(fastSchedule, slowSchedule) + * + * 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 < 3) { + * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) + * } + * + * return `Success on attempt ${attempt}` + * }), + * conservativeSchedule + * ) + * + * yield* Console.log(`Final result: ${result}`) + * }) + * + * // Both provides intersection semantics (AND logic) + * // Compare with either which provides union semantics (OR logic) + * ``` + * + * @see {@link either} for continuing while either schedule still recurs + * + * @category utils + * @since 2.0.0 + */ +export const both: { + ( + other: Schedule + ): ( + self: Schedule + ) => Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2> + ( + self: Schedule, + other: Schedule + ): Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2> +} = dual(2, ( + self: Schedule, + other: Schedule +): Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2> => + bothWith(self, other, (left, right) => [left, right])) + +/** + * Combines two `Schedule`s by recurring if both of the two schedules want + * to recur, using the maximum of the two durations between recurrences and + * outputting the result of the left schedule (i.e. `self`). + * + * **Example** (Combining schedules and keeping the left output) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Combine two schedules, keeping left output + * const leftSchedule = Schedule.exponential("100 millis").pipe( + * Schedule.map(() => Effect.succeed("left-result")) + * ) + * const rightSchedule = Schedule.spaced("50 millis") + * + * const combined = Schedule.bothLeft(leftSchedule, rightSchedule) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task-done" + * }), + * combined.pipe(Schedule.take(3)) + * ) + * }) + * ``` + * + * @category utils + * @since 2.0.0 + */ +export const bothLeft: { + ( + other: Schedule + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + other: Schedule + ): Schedule +} = dual(2, ( + self: Schedule, + other: Schedule +): Schedule => bothWith(self, other, (output) => output)) + +/** + * Combines two `Schedule`s by recurring if both of the two schedules want + * to recur, using the maximum of the two durations between recurrences and + * outputting the result of the right schedule (i.e. `other`). + * + * **Example** (Combining schedules and keeping the right output) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Combine two schedules, keeping right output + * const leftSchedule = Schedule.exponential("100 millis").pipe( + * Schedule.map(() => Effect.succeed("left-result")) + * ) + * const rightSchedule = Schedule.spaced("50 millis").pipe( + * Schedule.map(() => Effect.succeed("right-result")) + * ) + * + * const combined = Schedule.bothRight(leftSchedule, rightSchedule) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task-done" + * }), + * combined.pipe(Schedule.take(3)) + * ) + * }) + * ``` + * + * @category utils + * @since 2.0.0 + */ +export const bothRight: { + ( + other: Schedule + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + other: Schedule + ): Schedule +} = dual(2, ( + self: Schedule, + other: Schedule +): Schedule => bothWith(self, other, (_, output) => output)) + +/** + * Combines two `Schedule`s by recurring if both of the two schedules want + * to recur, using the maximum of the two durations between recurrences and + * outputting the result of the combination of both schedule outputs using the + * specified `combine` function. + * + * **Example** (Combining schedule outputs) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Combine two schedules with custom output combination + * const leftSchedule = Schedule.exponential("100 millis").pipe( + * Schedule.map(() => Effect.succeed("left")) + * ) + * const rightSchedule = Schedule.spaced("50 millis").pipe( + * Schedule.map(() => Effect.succeed("right")) + * ) + * + * const combined = Schedule.bothWith( + * leftSchedule, + * rightSchedule, + * (left, right) => `${left}-${right}` + * ) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task-result" + * }), + * combined.pipe(Schedule.take(3)) + * ) + * }) + * ``` + * + * @category utils + * @since 2.0.0 + */ +export const bothWith: { + ( + other: Schedule, + combine: (selfOutput: Output, otherOutput: Output2) => Output3 + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + other: Schedule, + combine: (selfOutput: Output, otherOutput: Output2) => Output3 + ): Schedule +} = dual(3, ( + self: Schedule, + other: Schedule, + combine: (selfOutput: Output, otherOutput: Output2) => Output3 +): Schedule => + fromStep(effect.map( + effect.zip(toStep(self), toStep(other)), + ([stepLeft, stepRight]) => (now, input) => + Pull.matchEffect(stepLeft(now, input as Input), { + onSuccess: (leftResult) => + stepRight(now, input as Input2).pipe( + effect.map((rightResult) => + [ + combine(leftResult[0], rightResult[0]), + Duration.max(leftResult[1], rightResult[1]) + ] as [Output3, Duration.Duration] + ), + Pull.catchDone((rightDone) => Cause.done(combine(leftResult[0], rightDone as Output2))) + ), + onDone: (leftDone) => + stepRight(now, input as Input2).pipe( + effect.flatMap((rightResult) => Cause.done(combine(leftDone, rightResult[0]))), + Pull.catchDone((rightDone) => Cause.done(combine(leftDone, rightDone as Output2))) + ), + onFailure: effect.failCause + }) + ))) + +/** + * Returns a new `Schedule` that follows `self` and outputs the inputs seen so + * far as an array. + * + * **Details** + * + * This does not make the schedule run forever. The collected schedule stops + * when `self` stops and fails when `self` fails. + * + * **Example** (Collecting schedule inputs) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Collect all inputs passed to the schedule + * const inputCollector = Schedule.collectInputs( + * Schedule.spaced("100 millis") + * ) + * + * const program = Effect.gen(function*() { + * let counter = 0 + * yield* Effect.repeat( + * Effect.gen(function*() { + * counter++ + * yield* Console.log(`Iteration ${counter}`) + * return `result-${counter}` + * }), + * inputCollector.pipe(Schedule.take(4)) + * ) + * }) + * ``` + * + * @category utils + * @since 4.0.0 + */ +export const collectInputs = ( + self: Schedule +): Schedule, Input, Error, Env> => collectWhile(passthrough(self), () => effect.succeed(true)) + +/** + * Returns a new `Schedule` that follows `self` and outputs the schedule outputs + * seen so far as an array. + * + * **Details** + * + * This does not make the schedule run forever. The collected schedule stops + * when `self` stops and fails when `self` fails. + * + * **Example** (Collecting schedule outputs) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Collect all outputs from the schedule + * const outputCollector = Schedule.collectOutputs( + * Schedule.recurs(4) + * ) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task-result" + * }), + * outputCollector.pipe(Schedule.take(4)) + * ) + * }) + * ``` + * + * @category utils + * @since 4.0.0 + */ +export const collectOutputs = ( + self: Schedule +): Schedule, Input, Error, Env> => collectWhile(self, () => effect.succeed(true)) + +/** + * Returns a new `Schedule` that recurs as long as the specified `predicate` + * returns `true`, collecting all outputs of the schedule into an array. + * + * **Example** (Collecting outputs while a condition holds) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Collect outputs while condition is met + * const collectWhileSmall = Schedule.collectWhile( + * Schedule.exponential("100 millis"), + * (metadata) => + * Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 2000) + * ) + * + * const conditionalProgram = Effect.gen(function*() { + * let attempt = 0 + * + * const attempts = yield* Effect.repeat( + * Effect.gen(function*() { + * attempt++ + * yield* Console.log(`Retry attempt ${attempt}`) + * return `attempt-${attempt}` + * }), + * collectWhileSmall + * ) + * + * yield* Console.log(`Collected attempts: [${attempts.join(", ")}]`) + * }) + * + * // Collect with effectful predicate + * const collectWithCheck = Schedule.collectWhile( + * Schedule.fixed("1 second"), + * (metadata) => + * Effect.gen(function*() { + * const shouldContinue = metadata.attempt < 5 + * yield* Console.log( + * `Check ${metadata.attempt}: continue = ${shouldContinue}` + * ) + * return shouldContinue + * }) + * ) + * + * const effectfulProgram = Effect.gen(function*() { + * const results = yield* Effect.repeat( + * Effect.succeed("checked"), + * collectWithCheck + * ) + * + * yield* Console.log(`Final collection: ${results.length} items`) + * }) + * + * // Collect samples with condition + * const samples = [12, 18, 24, 30, 36] + * + * const collectSamples = Schedule.collectWhile( + * Schedule.spaced("200 millis"), + * (metadata) => + * Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 2000) + * ) + * + * const samplingProgram = Effect.gen(function*() { + * let index = 0 + * const collected = yield* Effect.repeat( + * Effect.gen(function*() { + * const sample = samples[index++] + * yield* Console.log(`Sample: ${sample}`) + * return sample + * }), + * collectSamples + * ) + * + * const average = collected.reduce((sum, s) => sum + s, 0) / collected.length + * yield* Console.log( + * `Collected ${collected.length} samples, average: ${average.toFixed(1)}` + * ) + * }) + * ``` + * + * @category utils + * @since 2.0.0 + */ +export const collectWhile: { + ( + predicate: ( + metadata: Metadata + ) => boolean | Effect + ): ( + self: Schedule + ) => Schedule, Input, Error | Error2, Env | Env2> + ( + self: Schedule, + predicate: ( + metadata: Metadata + ) => boolean | Effect + ): Schedule, Input, Error | Error2, Env | Env2> +} = dual(2, ( + self: Schedule, + predicate: ( + metadata: Metadata + ) => boolean | Effect +): Schedule, Input, Error | Error2, Env | Env2> => + reduce(while_(self, predicate), () => [] as Array, (outputs, output) => { + outputs.push(output) + return outputs + })) + +/** + * 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.take(3), // Run only 3 times for demo + * Schedule.tapOutput((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.take(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.take(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.take(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.take(1)) + * ) + * }) + * + * // Complex cron with error handling + * const complexCron = Schedule.cron("0 2,4,6 * * *").pipe( + * Schedule.tapOutput((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.take(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, _) => + effect.sync(() => { + const next = Cron.next(cron, now).getTime() + const duration = Duration.millis(next - now) + return [duration, duration] + }))) +} + +/** + * Returns a new schedule that outputs the delay between each occurrence. + * + * **Example** (Extracting schedule delays) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Extract delays from an exponential backoff schedule + * const exponentialDelays = Schedule.delays( + * Schedule.exponential("100 millis").pipe(Schedule.take(5)) + * ) + * + * const delayProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task result" + * }), + * exponentialDelays.pipe( + * Schedule.tapOutput((delay) => + * Console.log(`Waiting ${delay} before next execution`) + * ) + * ) + * ) + * }) + * + * // Monitor delays from a Fibonacci schedule + * const fibonacciDelays = Schedule.delays( + * Schedule.fibonacci("200 millis").pipe(Schedule.take(8)) + * ) + * + * const fibDelayProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Console.log("Fibonacci task"), + * fibonacciDelays.pipe( + * Schedule.tapOutput((delay) => Console.log(`Fibonacci delay: ${delay}`)) + * ) + * ) + * }) + * + * // Extract delays for analysis or logging + * const analyzeDelays = Schedule.delays( + * Schedule.spaced("1 second").pipe(Schedule.take(3)) + * ).pipe( + * Schedule.tapOutput((delay) => + * Effect.gen(function*() { + * yield* Console.log(`Recorded delay: ${delay}`) + * // In real applications, might send to metrics system + * }) + * ) + * ) + * + * // Combine delays with other schedules for complex timing + * const adaptiveSchedule = Schedule.unfold(100, (delay) => Effect.succeed(delay * 1.5)).pipe( + * Schedule.take(6) + * ) + * + * const adaptiveDelays = Schedule.delays(adaptiveSchedule) + * + * const adaptiveProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Adaptive task execution") + * return "completed" + * }), + * adaptiveDelays.pipe( + * Schedule.tapOutput((delay) => Console.log(`Adaptive delay: ${delay}`)) + * ) + * ) + * }) + * + * // Use delays to implement custom timing logic + * const customTimingSchedule = Schedule.delays( + * Schedule.exponential("50 millis").pipe(Schedule.take(4)) + * ).pipe( + * Schedule.map((delay) => Effect.succeed(`Next execution in ${delay}`)), + * Schedule.tapOutput((message) => Console.log(message)) + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const delays = (self: Schedule): Schedule => + fromStep( + effect.map( + toStep(self), + (step) => (now, input) => + Pull.catchDone( + effect.map(step(now, input), ([_, duration]) => [duration, duration]), + (_) => Cause.done(Duration.zero) + ) + ) + ) + +/** + * Returns a schedule that recurs once after the specified duration. + * + * **When to use** + * + * Use when you need one delayed recurrence. Use `during` to keep + * recurring until a duration has elapsed. + * + * **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. Use + * `duration` when you need one delayed recurrence. + * + * **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.tapOutput((elapsedDuration) => + * Console.log(`Total elapsed: ${elapsedDuration}`) + * ) + * ) + * ) + * + * yield* Console.log("Time limit reached!") + * }) + * + * // Combine with other schedules for time-bounded execution + * const timeAndCountLimited = Schedule.spaced("1 second").pipe( + * Schedule.both(Schedule.during("10 seconds")), // Stop after 10 seconds OR + * Schedule.both(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.exponential("200 millis").pipe( + * Schedule.both(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 => + while_( + elapsed, + ({ output }) => effect.succeed(Duration.isLessThanOrEqualTo(output, Duration.fromInputUnsafe(duration))) + ) + +/** + * Combines two `Schedule`s by recurring if either of the two schedules wants + * to recur, using the minimum of the two durations between recurrences and + * outputting a tuple of the outputs of both schedules. + * + * **When to use** + * + * Use when the combined schedule should continue while at least one + * schedule still recurs. Use `both` when both schedules must continue. + * + * **Example** (Combining schedules with either semantics) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * // Either continues as long as at least one schedule wants to continue + * const timeBasedSchedule = Schedule.spaced("2 seconds").pipe(Schedule.take(3)) + * const countBasedSchedule = Schedule.recurs(5) + * + * // Continues until both schedules are exhausted (either still wants to recur) + * const eitherSchedule = Schedule.either(timeBasedSchedule, countBasedSchedule) + * // Outputs: [time_result, count_result] tuple + * + * const program = Effect.gen(function*() { + * const results = yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task completed" + * }), + * eitherSchedule.pipe( + * Schedule.tapOutput(([timeResult, countResult]) => + * Console.log(`Time: ${timeResult}, Count: ${countResult}`) + * ) + * ) + * ) + * + * yield* Console.log(`Total executions: ${results.length}`) + * }) + * + * // Either with different delay strategies + * const aggressiveRetry = Schedule.exponential("100 millis").pipe( + * Schedule.take(3) + * ) + * const fallbackRetry = Schedule.fixed("5 seconds").pipe(Schedule.take(2)) + * + * // Will use the more aggressive retry until it's exhausted, then fallback + * const combinedRetry = Schedule.either(aggressiveRetry, fallbackRetry) + * + * 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 < 6) { + * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) + * } + * + * return `Success on attempt ${attempt}` + * }), + * combinedRetry + * ) + * + * yield* Console.log(`Final result: ${result}`) + * }) + * + * // Either provides union semantics (OR logic) + * // Compare with both, which provides intersection semantics (AND logic) + * ``` + * + * @see {@link both} for continuing only while both schedules still recur + * + * @category utils + * @since 2.0.0 + */ +export const either: { + ( + other: Schedule + ): ( + self: Schedule + ) => Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2> + ( + self: Schedule, + other: Schedule + ): Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2> +} = dual(2, ( + self: Schedule, + other: Schedule +): Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2> => + eitherWith(self, other, (left, right) => [left, right])) + +/** + * Combines two `Schedule`s by recurring if either of the two schedules wants + * to recur, using the minimum of the two durations between recurrences and + * outputting the result of the left schedule (i.e. `self`). + * + * **Example** (Combining either schedules and keeping the left output) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Combine two schedules with either semantics, keeping left output + * const primarySchedule = Schedule.exponential("100 millis").pipe( + * Schedule.map(() => Effect.succeed("primary-result")), + * Schedule.take(2) + * ) + * const backupSchedule = Schedule.spaced("500 millis").pipe( + * Schedule.map(() => Effect.succeed("backup-result")) + * ) + * + * const combined = Schedule.eitherLeft(primarySchedule, backupSchedule) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task-done" + * }), + * combined.pipe(Schedule.take(5)) + * ) + * }) + * ``` + * + * @category utils + * @since 4.0.0 + */ +export const eitherLeft: { + ( + other: Schedule + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + other: Schedule + ): Schedule +} = dual(2, ( + self: Schedule, + other: Schedule +): Schedule => eitherWith(self, other, (output) => output)) + +/** + * Combines two `Schedule`s by recurring if either of the two schedules wants + * to recur, using the minimum of the two durations between recurrences and + * outputting the result of the right schedule (i.e. `other`). + * + * **Example** (Combining either schedules and keeping the right output) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Combine two schedules with either semantics, keeping right output + * const primarySchedule = Schedule.exponential("100 millis").pipe( + * Schedule.map(() => Effect.succeed("primary-result")), + * Schedule.take(2) + * ) + * const backupSchedule = Schedule.spaced("500 millis").pipe( + * Schedule.map(() => Effect.succeed("backup-result")) + * ) + * + * const combined = Schedule.eitherRight(primarySchedule, backupSchedule) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task-done" + * }), + * combined.pipe(Schedule.take(5)) + * ) + * }) + * ``` + * + * @category utils + * @since 4.0.0 + */ +export const eitherRight: { + ( + other: Schedule + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + other: Schedule + ): Schedule +} = dual(2, ( + self: Schedule, + other: Schedule +): Schedule => eitherWith(self, other, (_, output) => output)) + +/** + * Combines two `Schedule`s by recurring if either of the two schedules wants + * to recur, using the minimum of the two durations between recurrences and + * outputting the result of the combination of both schedule outputs using the + * specified `combine` function. + * + * **Example** (Combining either schedule outputs) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Combine schedules with either semantics and custom combination + * const primarySchedule = Schedule.exponential("100 millis").pipe( + * Schedule.map(() => Effect.succeed("primary")), + * Schedule.take(2) + * ) + * const fallbackSchedule = Schedule.spaced("500 millis").pipe( + * Schedule.map(() => Effect.succeed("fallback")) + * ) + * + * const combined = Schedule.eitherWith( + * primarySchedule, + * fallbackSchedule, + * (primary, fallback) => `${primary}+${fallback}` + * ) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task-result" + * }), + * combined.pipe(Schedule.take(5)) + * ) + * }) + * ``` + * + * @category utils + * @since 2.0.0 + */ +export const eitherWith: { + ( + other: Schedule, + combine: (selfOutput: Output, otherOutput: Output2) => Output3 + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + other: Schedule, + combine: (selfOutput: Output, otherOutput: Output2) => Output3 + ): Schedule +} = dual(3, ( + self: Schedule, + other: Schedule, + combine: (selfOutput: Output, otherOutput: Output2) => Output3 +): Schedule => + fromStep(effect.map( + effect.zip(toStep(self), toStep(other)), + ([stepLeft, stepRight]) => (now, input) => + Pull.matchEffect(stepLeft(now, input as Input), { + onSuccess: (leftResult) => + stepRight(now, input as Input2).pipe( + effect.map((rightResult) => + [combine(leftResult[0], rightResult[0]), Duration.min(leftResult[1], rightResult[1])] as [ + Output3, + Duration.Duration + ] + ), + Pull.catchDone((rightDone) => + effect.succeed<[Output3, Duration.Duration]>([ + combine(leftResult[0], rightDone as Output2), + leftResult[1] + ]) + ) + ), + onFailure: effect.failCause, + onDone: (leftDone) => + stepRight(now, input as Input2).pipe( + effect.map((rightResult) => + [combine(leftDone, rightResult[0]), rightResult[1]] as [ + Output3, + Duration.Duration + ] + ), + Pull.catchDone((rightDone) => Cause.done(combine(leftDone, rightDone as Output2))) + ) + }) + ))) + +/** + * Schedule that always recurs and returns the total elapsed duration since the + * first recurrence. + * + * **Details** + * + * This schedule never stops and outputs the cumulative time that has passed since the schedule + * started executing. Useful for tracking execution time or implementing time-based logic. + * + * **Example** (Measuring elapsed schedule time) + * + * ```ts + * import { Console, Duration, Effect, Schedule } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Console.log("Running task..."), + * Schedule.spaced("1 second").pipe( + * Schedule.both(Schedule.elapsed), + * Schedule.tapOutput(([count, duration]) => + * Console.log(`Run ${count}, elapsed: ${Duration.toMillis(duration)}ms`) + * ), + * Schedule.take(5) + * ) + * ) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const elapsed: Schedule = fromStepWithMetadata( + effect.succeed((meta) => effect.succeed([Duration.millis(meta.elapsed), Duration.zero] as const)) +) + +/** + * 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.exponential("50 millis").pipe( + * Schedule.both(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.fibonacci("50 millis").pipe( + * Schedule.both(Schedule.recurs(6)), // Maximum 6 retries + * Schedule.tapOutput((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.take(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. Use + * `spaced` when each delay should start after the previous action completes. + * + * **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.take(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.take(3))) + * ) + * + * // Comparing with spaced (waits 1s AFTER each task) + * const spacedSchedule = longRunningTask.pipe( + * Effect.repeat(Schedule.spaced("1 second").pipe(Schedule.take(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 the output of this schedule using the + * specified function. + * + * **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((count) => Effect.succeed(`Execution #${count + 1}`)) + * ) + * + * // Map schedule delays to human-readable format + * const readableDelays = Schedule.exponential("100 millis").pipe( + * Schedule.map((duration) => Effect.succeed(`Next retry in ${duration}`)) + * ) + * + * // Transform numeric output to structured data + * const structuredSchedule = Schedule.spaced("1 second").pipe( + * Schedule.map((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.take(8), + * Schedule.tapOutput((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((count) => + * Effect.gen(function*() { + * yield* Console.log(`Processing count: ${count}`) + * return count * 10 + * }) + * ) + * ) + * + * // Combine mapping with other schedule operations + * const complexSchedule = Schedule.fibonacci("100 millis").pipe( + * Schedule.map((delay) => Effect.succeed(`Delay: ${delay}`)) + * ) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + ( + f: (output: Output) => Output2 | Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + f: (output: Output) => Output2 | Effect + ): Schedule +} = dual(2, ( + self: Schedule, + f: (output: Output) => Output2 | Effect +): Schedule => { + const handle = Pull.matchEffect({ + onSuccess: ([output, duration]: [Output, Duration.Duration]) => { + const result = f(output) + 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: Output) => { + const result = f(output) + if (!isEffect(result)) return Cause.done(result as Output2) + return effect.flatMap(result, Cause.done) + } + }) + return fromStep(effect.map(toStep(self), (step) => (now, input) => handle(step(now, input)))) +}) + +/** + * 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 output) + * + * ```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, delay) => { + * // Double the delay if we're seeing high iteration counts + * return Effect.succeed(output > 5 ? Duration.times(delay, 2) : delay) + * }) + * ) + * + * 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.take(8)) + * ) + * }) + * ``` + * + * @category utils + * @since 2.0.0 + */ +export const modifyDelay: { + ( + f: ( + output: Output, + delay: Duration.Duration + ) => Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + f: ( + output: Output, + delay: Duration.Input + ) => Effect + ): Schedule +} = dual(2, ( + self: Schedule, + f: ( + output: Output, + delay: Duration.Input + ) => Effect +): Schedule => + fromStep(effect.map(toStep(self), (step) => (now, input) => + effect.flatMap( + step(now, input), + ([output, delay]) => effect.map(f(output, delay), (delay) => [output, Duration.fromInputUnsafe(delay)]) + )))) + +/** + * 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 utils + * @since 2.0.0 + */ +export const jittered = ( + self: Schedule +): Schedule => + modifyDelay(self, (_, delay) => + effect.map(randomNext, (random) => { + const millis = Duration.toMillis(Duration.fromInputUnsafe(delay)) + 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.take(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 utils + * @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. Use `take` + * to limit an existing schedule while preserving its output and delay + * behavior. + * + * **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.exponential("100 millis").pipe( + * Schedule.both(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.tapOutput((count) => Console.log(`Execution #${count + 1}`)) + * ) + * ``` + * + * @see {@link take} 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 new `Schedule` that combines the outputs of the provided schedule + * using the specified effectful `combine` function and starting from the + * specified `initial` state. + * + * **Example** (Reducing schedule outputs) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Sum up execution counts from a counter schedule + * const sumSchedule = Schedule.reduce( + * Schedule.recurs(5), + * () => 0, // Initial sum + * (sum, count) => Effect.succeed(sum + count) // Add each count to the sum + * ) + * + * const sumProgram = Effect.gen(function*() { + * const finalSum = yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task" + * }), + * sumSchedule.pipe( + * Schedule.tapOutput((sum) => Console.log(`Running sum: ${sum}`)) + * ) + * ) + * + * yield* Console.log(`Final sum: ${finalSum}`) + * }) + * + * // Build a history of execution counts + * const historySchedule = Schedule.reduce( + * Schedule.spaced("1 second").pipe(Schedule.take(4)), + * () => [] as Array, // Initial empty array + * (history, executionNumber) => Effect.succeed([...history, executionNumber]) + * ) + * + * const historyProgram = Effect.gen(function*() { + * const timeline = yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Recording execution...") + * return "recorded" + * }), + * historySchedule + * ) + * + * yield* Console.log( + * `Execution timeline: ${timeline.join(", ")}` + * ) + * }) + * + * // Accumulate metrics with effectful combination + * const metricsAccumulator = Schedule.reduce( + * Schedule.recurs(6), + * () => ({ total: 0, count: 0, max: 0 }), + * (metrics, executionCount) => Effect.succeed({ + * total: metrics.total + executionCount + 1, + * count: metrics.count + 1, + * max: Math.max(metrics.max, executionCount + 1) + * }) + * ) + * + * const metricsProgram = Effect.gen(function*() { + * const finalMetrics = yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Processing...") + * return "processed" + * }), + * metricsAccumulator + * ) + * + * const average = finalMetrics.total / finalMetrics.count + * yield* Console.log(`Final metrics: ${finalMetrics.count} executions`) + * yield* Console.log( + * `Average delay: ${average.toFixed(1)}ms, Max delay: ${finalMetrics.max}ms` + * ) + * }) + * + * // Build configuration state over time + * const configBuilder = Schedule.reduce( + * Schedule.fixed("500 millis").pipe(Schedule.take(3)), + * () => ({ retries: 1, timeout: 1000, backoff: 100 }), + * (config, executionNumber) => Effect.succeed({ + * retries: config.retries + 1, + * timeout: config.timeout * 1.5, + * backoff: Math.min(config.backoff * 2, 5000) + * }) + * ) + * + * const configProgram = Effect.gen(function*() { + * const finalConfig = yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Updating configuration...") + * return "updated" + * }), + * configBuilder.pipe( + * Schedule.tapOutput((config) => + * Console.log( + * `Config: retries=${config.retries}, timeout=${config.timeout}ms` + * ) + * ) + * ) + * ) + * + * yield* Console.log(`Final config: ${JSON.stringify(finalConfig)}`) + * }) + * ``` + * + * @category utils + * @since 2.0.0 + */ +export const reduce: { + ( + initial: LazyArg, + combine: (state: State, output: Output) => State | Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + initial: LazyArg, + combine: (state: State, output: Output) => State | Effect + ): Schedule +} = dual(3, ( + self: Schedule, + initial: LazyArg, + combine: (state: State, output: Output) => State | Effect +): Schedule => + fromStep(effect.map(toStep(self), (step) => { + let state = initial() + return (now, input) => + Pull.matchEffect(step(now, input), { + onSuccess([output, delay]) { + const next = combine(state, output) + if (!isEffect(next)) { + state = next + return effect.succeed([next, delay] as [State, Duration.Duration]) + } + return effect.map(next, (nextState) => { + state = nextState + return [nextState, delay] + }) + }, + onFailure: effect.failCause, + onDone(output) { + const next = combine(state, output) + return isEffect(next) ? effect.flatMap(next, Cause.done) : Cause.done(next) + } + }) + }))) + +/** + * 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. Use `fixed` when recurrences should stay aligned to a regular + * cadence. + * + * **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.take(5)) + * ) + * ) + * + * // Simple spaced schedule with limited repetitions + * const limitedSpaced = Schedule.spaced("100 millis").pipe( + * Schedule.both(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.take(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 allows execution of an effectful function for + * every input to the schedule, but does not alter the inputs and outputs of + * the schedule. + * + * **Example** (Tapping retry inputs) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryError extends Data.TaggedError("RetryError")<{ readonly message: string }> {} + * + * // Log retry errors for debugging + * const errorLoggingSchedule = Schedule.exponential("100 millis").pipe( + * Schedule.take(3), + * Schedule.tapInput((error: RetryError) => + * Console.log(`Retry triggered by error: ${String(error)}`) + * ) + * ) + * + * const retryProgram = Effect.gen(function*() { + * let attempt = 0 + * + * const result = yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * if (attempt < 4) { + * return yield* Effect.fail(new RetryError({ message: `Network timeout on attempt ${attempt}` })) + * } + * return `Success on attempt ${attempt}` + * }), + * errorLoggingSchedule + * ) + * + * yield* Console.log(`Final result: ${result}`) + * }) + * + * // Monitor input frequency for metrics + * const inputMonitoringSchedule = Schedule.spaced("1 second").pipe( + * Schedule.take(5), + * Schedule.tapInput((input: unknown) => + * Effect.gen(function*() { + * yield* Console.log(`Input type: ${typeof input}`) + * // In real applications, might send metrics to monitoring system + * }) + * ) + * ) + * + * // Input validation with side effects + * const validatingSchedule = Schedule.fixed("500 millis").pipe( + * Schedule.take(4), + * Schedule.tapInput((input: any) => + * Effect.gen(function*() { + * if (typeof input === "object" && input !== null) { + * yield* Console.log(`Valid object input: ${JSON.stringify(input)}`) + * } else { + * yield* Console.log(`Warning: Non-object input received: ${input}`) + * } + * }) + * ) + * ) + * + * const validationProgram = Effect.gen(function*() { + * let count = 0 + * + * yield* Effect.repeat( + * Effect.gen(function*() { + * count++ + * yield* Console.log("Task with validation") + * return { data: `sample-${count}` } + * }), + * validatingSchedule + * ) + * }) + * + * // Conditional alerting based on input + * const alertingSchedule = Schedule.exponential("200 millis").pipe( + * Schedule.take(6), + * Schedule.tapInput((error: RetryError) => + * Effect.gen(function*() { + * if (String(error).includes("critical")) { + * yield* Console.log(`Critical error: ${String(error)}`) + * // In real applications, might trigger alerts or notifications + * } else { + * yield* Console.log(`Regular error: ${String(error)}`) + * } + * }) + * ) + * ) + * + * const alertProgram = Effect.gen(function*() { + * let attempt = 0 + * + * yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * const isCritical = attempt === 3 + * const errorType = isCritical + * ? "critical database failure" + * : "temporary network issue" + * return yield* Effect.fail(new RetryError({ message: errorType })) + * }), + * alertingSchedule + * ).pipe( + * Effect.catch((error: unknown) => + * Console.log(`All retries exhausted: ${String(error)}`) + * ) + * ) + * }) + * + * // Chain multiple input taps for different purposes + * const comprehensiveSchedule = Schedule.fibonacci("100 millis").pipe( + * Schedule.take(5), + * Schedule.tapInput((error: RetryError) => + * Console.log(`Error occurred: ${error._tag}`) + * ), + * Schedule.tapInput((error: RetryError) => + * String(error).length > 20 + * ? Console.log("Long error message detected") + * : Effect.void + * ) + * ) + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const tapInput: { + ( + f: (input: Input) => Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + f: (input: Input) => Effect + ): Schedule +} = dual(2, ( + self: Schedule, + f: (input: Input) => Effect +): Schedule => + fromStep(effect.map( + toStep(self), + (step) => (now, input) => effect.andThen(f(input), step(now, input)) + ))) + +/** + * Returns a new `Schedule` that allows execution of an effectful function for + * every output of the schedule, but does not alter the inputs and outputs of + * the schedule. + * + * **Example** (Tapping schedule outputs) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * // Log schedule outputs for debugging/monitoring + * const monitoredSchedule = Schedule.exponential("100 millis").pipe( + * Schedule.take(5), + * Schedule.tapOutput((delay) => Console.log(`Next delay will be: ${delay}`)) + * ) + * + * const retryProgram = Effect.gen(function*() { + * let attempt = 0 + * + * const result = yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * if (attempt < 4) { + * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) + * } + * return `Success on attempt ${attempt}` + * }), + * monitoredSchedule + * ) + * + * yield* Console.log(`Final result: ${result}`) + * }) + * + * // Tap output for metrics collection + * const metricsSchedule = Schedule.spaced("1 second").pipe( + * Schedule.take(10), + * Schedule.tapOutput((executionCount) => + * Effect.gen(function*() { + * // Simulate metrics collection + * yield* Console.log(`Recording metric: execution_count=${executionCount}`) + * // In real code, this might send to monitoring system + * }) + * ) + * ) + * + * // Tap output with conditional side effects + * const alertingSchedule = Schedule.fibonacci("200 millis").pipe( + * Schedule.take(8), + * Schedule.tapOutput((delay) => + * Effect.gen(function*() { + * const delayMs = delay.toString() + * if (delayMs.includes("1000")) { // Alert on delays >= 1 second + * yield* Console.log(`High delay detected: ${delay}`) + * } + * }) + * ) + * ) + * + * const healthCheckProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Performing health check...") + * return "healthy" + * }), + * alertingSchedule + * ) + * }) + * + * // Chain multiple taps for different purposes + * const comprehensiveSchedule = Schedule.fixed("500 millis").pipe( + * Schedule.take(6), + * Schedule.tapOutput((count) => Console.log(`Execution ${count + 1}`)), + * Schedule.tapOutput((count) => + * count % 3 === 0 + * ? Console.log("Checkpoint reached") + * : Effect.void + * ) + * ) + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const tapOutput: { + ( + f: (output: Output) => Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + f: (output: Output) => Effect + ): Schedule +} = dual(2, ( + self: Schedule, + f: (output: Output) => Effect +): Schedule => + fromStep(effect.map( + toStep(self), + (step) => (now, input) => effect.tap(step(now, input), ([output]) => f(output)) + ))) + +/** + * Returns a new `Schedule` that takes at most the specified number of outputs + * from the schedule. Once the specified number of outputs is reached, the + * schedule will stop. + * + * **When to use** + * + * Use to limit an existing schedule while preserving its output and + * delay behavior. Use `recurs` when you only need an immediate counter + * schedule. + * + * **Gotchas** + * + * `take(n)` 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 number of outputs taken. + * + * **Example** (Taking a limited number of recurrences) + * + * ```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.take(5) // Will stop after 5 schedule outputs + * ) + * + * 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 to a specific number + * const limitedRetry = Schedule.exponential("100 millis").pipe( + * Schedule.take(3) // At most 3 retry attempts + * ) + * + * 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)}`) + * ) + * ) + * + * // Combine take with other schedule operations + * const samplingSchedule = Schedule.fixed("500 millis").pipe( + * Schedule.take(10), // Take at most 10 schedule outputs + * Schedule.map((count) => Effect.succeed(`Sample #${count + 1}`)) + * ) + * + * const samplingProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * const value = "sample" + * yield* Console.log(`Sampled value: ${value}`) + * return value + * }), + * samplingSchedule.pipe( + * Schedule.tapOutput((label) => Console.log(`Completed: ${label}`)) + * ) + * ) + * }) + * ``` + * + * @see {@link recurs} for creating a count-limited schedule + * + * @category utils + * @since 4.0.0 + */ +export const take: { + (n: number): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + n: number + ): Schedule +} = dual(2, ( + self: Schedule, + n: number +): Schedule => while_(self, ({ attempt }) => effect.succeed(attempt <= n))) + +/** + * Creates a schedule that unfolds a state by repeatedly applying a function, + * outputting the current state and computing the next state. + * + * **Example** (Unfolding schedule state) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Counter schedule that increments by 1 each time + * const counterSchedule = Schedule.unfold(0, (n) => Effect.succeed(n + 1)) + * // Outputs: 0, 1, 2, 3, 4, 5, ... + * + * const countingProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "done" + * }), + * counterSchedule.pipe( + * Schedule.take(5), + * Schedule.tapOutput((count) => Console.log(`Count: ${count}`)) + * ) + * ) + * }) + * + * // Fibonacci sequence schedule + * const fibonacciSchedule = Schedule.unfold( + * [0, 1] as [number, number], + * ([a, b]) => Effect.succeed([b, a + b] as [number, number]) + * ) + * // Outputs: [0,1], [1,1], [1,2], [2,3], [3,5], [5,8], ... + * + * const fibProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Console.log("Fibonacci step"), + * fibonacciSchedule.pipe( + * Schedule.take(8), + * Schedule.tapOutput(([a, b]) => Console.log(`Fib: ${a}, next: ${b}`)) + * ) + * ) + * }) + * + * // Effectful unfold - exponential backoff with state + * const exponentialState = Schedule.unfold( + * 100, + * (delayMs) => + * Effect.gen(function*() { + * yield* Console.log(`Current delay: ${delayMs}ms`) + * return Math.min(delayMs * 2, 5000) // Cap at 5 seconds + * }) + * ) + * + * // Deterministic delay adjustment schedule + * const adjustedDelaySchedule = Schedule.unfold( + * { delay: 1000, adjustment: 100 }, + * ({ delay, adjustment }) => + * Effect.gen(function*() { + * const nextDelay = Math.max(100, delay + adjustment) + * yield* Console.log(`Adjusted delay: ${nextDelay}ms`) + * return { delay: nextDelay, adjustment: adjustment * -1 } + * }) + * ) + * + * // State machine schedule + * type State = "init" | "warming" | "active" | "cooling" + * const stateMachineSchedule = Schedule.unfold("init" as State, (state) => { + * switch (state) { + * case "init": + * return Effect.succeed("warming" as State) + * case "warming": + * return Effect.succeed("active" as State) + * case "active": + * return Effect.succeed("cooling" as State) + * case "cooling": + * return Effect.succeed("active" as State) + * } + * }) + * + * const stateMachineProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("State machine step") + * return "step" + * }), + * stateMachineSchedule.pipe( + * Schedule.take(10), + * Schedule.tapOutput((state) => Console.log(`State: ${state}`)) + * ) + * ) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const unfold = ( + initial: State, + next: (state: State) => Effect +): Schedule => + fromStep(effect.sync(() => { + let state = initial + return constant(effect.map( + effect.suspend(() => next(state)), + (nextState) => { + const prev = state + state = nextState + return [prev, Duration.zero] as const + } + )) + })) + +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 collectWhile} for collecting outputs while using the same predicate + * @see {@link take} for stopping after a fixed number of schedule outputs + * + * @category utils + * @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.take(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.take(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 +} + +/** + * Ensures that a schedule's input type extends a given type `T`. + * + * **When to use** + * + * Use to check an existing schedule input type. Use + * `setInputType` 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** (Constraining schedule input types) + * + * ```ts + * import { Schedule } from "effect" + * + * declare const StringInputSchedule: Schedule.Schedule + * declare const NumberInputSchedule: Schedule.Schedule + * + * const satisfiesStringInput = Schedule.satisfiesInputType() + * + * // This works because the schedule input type is string. + * const validSchedule = satisfiesStringInput(StringInputSchedule) + * + * // This would cause a TypeScript compilation error: + * // const invalidSchedule = satisfiesStringInput(NumberInputSchedule) + * ``` + * + * @see {@link setInputType} for adapting an input-agnostic schedule + * + * @category utility types + * @since 4.0.0 + */ +export const satisfiesInputType = () => +( + self: Schedule +): Schedule => self + +/** + * 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. Use `satisfiesInputType` to check an existing schedule input type. + * + * **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() + * ) + * ``` + * + * @see {@link satisfiesInputType} for checking an existing input type + * + * @category utility types + * @since 4.0.0 + */ +export const setInputType = + () => (self: Schedule): Schedule => self + +/** + * Ensures that a schedule's output type extends a given type `T`. + * + * **Details** + * + * This helper is checked at compile time and does not change the schedule's + * runtime behavior. + * + * **Example** (Constraining schedule output types) + * + * ```ts + * import { Schedule } from "effect" + * + * declare const StringOutputSchedule: Schedule.Schedule + * declare const NumberOutputSchedule: Schedule.Schedule + * + * const satisfiesStringOutput = Schedule.satisfiesOutputType() + * + * // This works because the schedule output type is string. + * const validSchedule = satisfiesStringOutput(StringOutputSchedule) + * + * // This would cause a TypeScript compilation error: + * // const invalidSchedule = satisfiesStringOutput(NumberOutputSchedule) + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const satisfiesOutputType = () => +( + self: Schedule +): Schedule => self + +/** + * Ensures that a schedule's error type extends a given type `T`. + * + * **Details** + * + * This helper is checked at compile time and does not change the schedule's + * runtime behavior. + * + * **Example** (Constraining schedule error types) + * + * ```ts + * import { Data, Schedule } from "effect" + * + * // Create a custom error using Data.TaggedError + * class CustomError extends Data.TaggedError("CustomError")<{ + * message: string + * }> {} + * + * declare const CustomErrorSchedule: Schedule.Schedule + * declare const StringErrorSchedule: Schedule.Schedule + * + * const satisfiesCustomError = Schedule.satisfiesErrorType() + * + * // This works because the schedule error type is CustomError. + * const validSchedule = satisfiesCustomError(CustomErrorSchedule) + * + * // This would cause a TypeScript compilation error: + * // const invalidSchedule = satisfiesCustomError(StringErrorSchedule) + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const satisfiesErrorType = () => +( + self: Schedule +): Schedule => self + +/** + * Ensures that a schedule's context type extends a given type `T`. + * + * **Details** + * + * This helper is checked at compile time and does not change the schedule's + * runtime behavior. + * + * **Example** (Constraining schedule service types) + * + * ```ts + * import { Schedule } from "effect" + * + * interface Logger { + * readonly log: (message: string) => void + * } + * + * declare const LoggerSchedule: Schedule.Schedule + * declare const NumberSchedule: Schedule.Schedule + * + * const satisfiesLogger = Schedule.satisfiesServicesType() + * + * // This works because the schedule context type is Logger. + * const validSchedule = satisfiesLogger(LoggerSchedule) + * + * // This would cause a TypeScript compilation error: + * // const invalidSchedule = satisfiesLogger(NumberSchedule) + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const satisfiesServicesType = () => +( + self: Schedule +): Schedule => self diff --git a/.repos/effect-smol/packages/effect/src/Scheduler.ts b/.repos/effect-smol/packages/effect/src/Scheduler.ts new file mode 100644 index 00000000000..7f43ac4a75c --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Scheduler.ts @@ -0,0 +1,294 @@ +/** + * The `Scheduler` module defines the runtime scheduling services used by + * Effect fibers. A scheduler decides how runnable tasks are enqueued, when they + * are dispatched, and whether a fiber should yield after consuming its + * operation budget. + * + * **Common tasks** + * + * - Use {@link Scheduler} to provide a custom runtime scheduler + * - Use {@link MixedScheduler} for the default priority-aware scheduler + * - Use {@link MaxOpsBeforeYield} to tune fairness for CPU-bound fibers + * - Use {@link PreventSchedulerYield} only when a runtime should bypass yield checks + * + * **Gotchas** + * + * - Scheduler priorities affect the order of queued runtime tasks, not the + * semantic result of an `Effect` + * - Disabling scheduler yields can improve throughput for controlled workloads, + * but it can also let long-running fibers monopolize the JavaScript thread + * + * @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 to provide or override the scheduler used by the Effect runtime. + * + * **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 to create a dispatcher for enqueuing work through this scheduler. + * + * @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 operation + * budget that triggers a scheduler 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-smol/packages/effect/src/Schema.ts b/.repos/effect-smol/packages/effect/src/Schema.ts new file mode 100644 index 00000000000..397350657ce --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Schema.ts @@ -0,0 +1,14186 @@ +/** + * Define data shapes, validate unknown input, and transform values between formats. + * + * ## Mental model + * + * - **Schema** — a description of a data shape. Every schema carries a decoded + * *Type* (the value you work with) and an *Encoded* representation (the + * serialized form, e.g. JSON). + * - **Decoding** — turning unknown external data (API responses, form + * submissions, config files) into typed, validated values. + * - **Encoding** — turning typed values back into a serializable format. + * - **Codec** — a schema that tracks both Type and Encoded, so it can decode + * *and* encode. Most concrete schemas are Codecs. + * - **Check / Filter** — a constraint attached to a schema (e.g. `isMinLength`, + * `isGreaterThan`). Attach them with `.check(...)`. + * - **Transformation** — a pair of functions (decode + encode) that convert + * values between two schemas. Created with {@link decodeTo} / {@link encodeTo}. + * - **Annotation** — metadata attached to a schema (title, description, custom + * keys). Attach with `.annotate(...)`. + * + * ## Common tasks + * + * - Define a struct: {@link Struct} + * - Define a union: {@link Union}, {@link TaggedUnion}, {@link Literals} + * - Define an array: {@link ArraySchema}, {@link NonEmptyArray} + * - Define a record: {@link Record} + * - Define a tuple: {@link Tuple}, {@link TupleWithRest} + * - Validate unknown data synchronously: {@link decodeUnknownSync} + * - Validate unknown data (Effect): {@link decodeUnknownEffect} + * - Encode a value: {@link encodeUnknownSync}, {@link encodeUnknownEffect} + * - Type guard: {@link is} + * - Assertion: {@link asserts} + * - Add constraints: `.check(...)` with filters like {@link isMinLength}, + * {@link isGreaterThan}, {@link isPattern}, {@link isUUID} + * - Transform between schemas: {@link decodeTo}, {@link encodeTo} + * - Add a default for missing keys: {@link withDecodingDefault}, {@link withDecodingDefaultKey} + * - Create branded types: {@link brand} + * - Define classes with validation: {@link Class}, {@link TaggedClass} + * - Define error classes: {@link ErrorClass}, {@link TaggedErrorClass} + * - Generate JSON Schema: {@link toJsonSchemaDocument} + * - Generate test data: {@link toArbitrary} + * - Derive equivalence: {@link toEquivalence} + * + * ## Gotchas + * + * - `Schema.optional` creates `T | undefined` (key can be missing *or* + * `undefined`). Use `Schema.optionalKey` for exact optional properties. + * - `decodeTo` is curried: use `from.pipe(Schema.decodeTo(to, ...))`. + * - `decodeUnknownSync` throws on failure. Use `decodeUnknownExit` or + * `decodeUnknownOption` for non-throwing alternatives. + * - Filters do not change the TypeScript type. Use {@link refine} or + * {@link brand} to narrow the type. + * - Recursive schemas require {@link suspend} to avoid infinite loops. + * + * ## Quickstart + * + * **Example** (Validate a user object) + * + * ```ts + * import { Schema } from "effect" + * + * const User = Schema.Struct({ + * name: Schema.String.check(Schema.isMinLength(1)), + * age: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)), + * email: Schema.optionalKey(Schema.String) + * }) + * + * // Decode unknown input — throws on failure + * const user = Schema.decodeUnknownSync(User)({ + * name: "Alice", + * age: 30 + * }) + * + * console.log(user) + * // { name: "Alice", age: 30 } + * ``` + * + * @see {@link Schema} — type-level view tracking only the decoded Type + * @see {@link Codec} — type-level view tracking both Type and Encoded + * @see {@link Struct} — define object shapes + * @see {@link decodeUnknownSync} — synchronous validation + * @see {@link decodeTo} — schema transformations + * + * @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 { SchemaError } 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 AST from "./SchemaAST.ts" +import * as Getter from "./SchemaGetter.ts" +import * as Issue from "./SchemaIssue.ts" +import * as Parser from "./SchemaParser.ts" +import type * as SchemaRepresentation from "./SchemaRepresentation.ts" +import * as Transformation 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 models + * @since 3.13.4 + */ +export interface MakeOptions { + /** + * The parse options to use for the schema. + */ + readonly parseOptions?: AST.ParseOptions | undefined + /** + * Whether to disable validation for the schema. + */ + readonly disableChecks?: boolean | undefined +} + +/** + * 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. + * - In user code, prefer {@link Schema}, {@link Codec}, {@link Decoder}, or + * {@link Encoder} instead. + * + * @see {@link Top} — the existential "any schema" type (erased type params) + * @see {@link Schema} — tracks only the decoded Type + * @see {@link Codec} — tracks Type + Encoded + * + * @category models + * @since 4.0.0 + */ +export interface Bottom< + out T, + out E, + out RD, + out RE, + out Ast extends AST.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 [AST.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. + * + * @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`. + * + * @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 +} + +/** + * The schema type returned by {@link declareConstructor}, tracking the decoded + * type `T`, the encoded type `E`, and the list of type-parameter schemas + * `TypeParameters`. + * + * @category constructors + * @since 4.0.0 + */ +export interface declareConstructor, Iso = T> extends + Bottom< + T, + E, + TypeParameters[number]["DecodingServices"], + TypeParameters[number]["EncodingServices"], + AST.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. + * + * **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 Issue.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: AST.Declaration, options: AST.ParseOptions) => Effect.Effect, + annotations?: Annotations.Declaration + ): declareConstructor => { + return make( + new AST.Declaration( + typeParameters.map(AST.getAST), + (typeParameters) => run(typeParameters.map((ast) => make(ast)) as any), + annotations + ) + ) + } +} + +/** + * The schema type returned by {@link declare}, representing a non-parametric + * opaque type `T` with no type parameters. + * + * @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. + * + * **Details** + * + * Use this when the type has no type parameters. For parametric types such as + * `Option` or `Array`, use {@link declareConstructor} instead. + * + * **Example** (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 Issue.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 utils + * @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** (Custom 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(AST.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 Decoder}`` — for decode-only APIs + * - {@link Encoder}`` — for encode-only APIs + * + * @category models + * @since 4.0.0 + */ +export interface Top extends + Bottom< + unknown, + unknown, + unknown, + unknown, + AST.AST, + Top, + unknown, + unknown, + any, // this is because TypeParameters is invariant + unknown, + Mutability, + Optionality, + ConstructorDefault, + Mutability, + Optionality + > +{} + +/** + * 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 Top ? S["Type"] : 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** (Function that accepts 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 Top ? S["Encoded"] : 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 Top ? S["DecodingServices"] : 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 Top ? S["EncodingServices"] : never +} + +/** + * 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 +} + +/** + * 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 {@link Codec} view for APIs that only *decode* (parse/validate) values. + * + * **Details** + * + * Use `Decoder` to accept "any schema that can decode to `T`" without + * constraining or depending on the encoded representation (`Encoded` is + * `unknown`) or encoding services. + * + * **Example** (Function that only needs to decode) + * + * ```ts + * import { Schema } from "effect" + * + * declare function validate(decoder: Schema.Decoder): (input: unknown) => T + * + * validate(Schema.String) // ok + * validate(Schema.NumberFromString) // ok + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface Decoder extends Codec { + readonly "Rebuild": Decoder +} + +/** + * A {@link Codec} view for APIs that only *encode* values. + * + * **Details** + * + * Use `Encoder` to accept "any schema that can encode to `E`" without + * constraining or depending on the decoded `Type` (`Type` is `unknown`) or + * decoding services. + * + * **Example** (Function that only needs to encode) + * + * ```ts + * import { Schema } from "effect" + * + * declare function serialize(encoder: Schema.Encoder): (value: unknown) => E + * + * serialize(Schema.String) // ok — encodes to string + * serialize(Schema.NumberFromString) // ok — encodes number to string + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface Encoder extends Codec { + 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 utils + * @since 4.0.0 + */ +export function revealCodec(codec: Codec) { + return codec +} + +export { + /** + * Error thrown (or returned as the error channel value) when schema decoding + * or encoding fails. + * + * **Details** + * + * The `issue` field contains a structured {@link Issue.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 +} + +/** + * Returns `true` if `u` is a {@link SchemaError}. + * + * **Example** (Type guard 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 + */ +export function isSchemaError(u: unknown): u is SchemaError { + return Predicate.hasProperty(u, InternalSchema.SchemaErrorTypeId) +} + +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?: Issue.LeafHook | undefined + readonly checkHook?: Issue.CheckHook | undefined + readonly parseOptions?: AST.ParseOptions | undefined + } +): StandardSchemaV1 & S { + const decodeUnknownEffect = Parser.decodeUnknownEffect(self) as ( + input: unknown, + options?: AST.ParseOptions + ) => Effect.Effect + const parseOptions: AST.ParseOptions = { errors: "all", ...options?.parseOptions } + const formatter = Issue.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: Top, 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. It's particularly useful for + * runtime type validation and TypeScript type narrowing. + * + * **Example** (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 = Parser.is + +/** + * Creates an assertion function that throws an error if the input doesn't match + * the schema. + * + * **When to use** + * + * Use to validate unknown input at runtime while narrowing the value with a + * TypeScript `asserts` predicate. + * + * **Details** + * + * The input is narrowed if the assertion succeeds. If validation fails, the + * assertion throws. + * + * **Example** (Basic Usage) + * + * ```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"] = Parser.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 decoding input whose type is not statically known. + * + * **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. + * + * @category decoding + * @since 4.0.0 + */ +export function decodeUnknownEffect(schema: S, options?: AST.ParseOptions) { + const parser = Parser.decodeUnknownEffect(schema, options) + return (input: unknown, options?: AST.ParseOptions): Effect.Effect => { + return Effect.mapErrorEager(parser(input, options), (issue) => new SchemaError(issue)) + } +} + +/** + * Decodes a typed input (the schema's `Encoded` type) against a schema, + * returning an `Effect` that succeeds with the decoded value or fails with a + * {@link SchemaError}. + * + * **When to use** + * + * Use when the input is already typed as the schema's `Encoded` type. + * + * **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. + * + * @category decoding + * @since 4.0.0 + */ +export const decodeEffect: ( + schema: S, + options?: AST.ParseOptions +) => (input: S["Encoded"], options?: AST.ParseOptions) => Effect.Effect = + decodeUnknownEffect + +/** + * Decodes an `unknown` input against a schema synchronously, returning an + * `Exit` that is either a `Success` with the decoded value or a `Failure` with + * a {@link SchemaError}. + * + * **When to use** + * + * Use when the input type is not statically known and decoding should return an + * `Exit` instead of failing or throwing. + * + * **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. + * + * @category decoding + * @since 4.0.0 + */ +export function decodeUnknownExit>(schema: S, options?: AST.ParseOptions) { + const parser = Parser.decodeUnknownExit(schema, options) + return (input: unknown, options?: AST.ParseOptions): Exit_.Exit => { + return Exit_.mapError(parser(input, options), (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` with a {@link SchemaError}. + * + * **When to use** + * + * Use when typed input should be decoded into an `Exit` instead of failing or + * throwing. + * + * **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. + * + * @category decoding + * @since 4.0.0 + */ +export const decodeExit: >( + schema: S, + options?: AST.ParseOptions +) => (input: S["Encoded"], options?: AST.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` on failure. + * + * **When to use** + * + * Use when the input type is not statically known and you only need to know + * whether decoding succeeded. + * + * **Details** + * + * Prefer this over {@link decodeUnknownExit} or {@link decodeUnknownEffect} + * when you don't need error details. For typed input use {@link decodeOption}. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknownOption = Parser.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` on failure. + * + * **When to use** + * + * Use when typed input should be decoded and only success or failure matters. + * + * **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. + * + * @category decoding + * @since 3.10.0 + */ +export const decodeOption = Parser.decodeOption + +/** + * Decodes an `unknown` input against a schema, returning a `Result` that + * succeeds with the decoded value or fails with a schema issue. + * + * **When to use** + * + * Use when the input type is not statically known and decoding should return a + * `Result` with structured issue data. + * + * **Details** + * + * For typed input use {@link decodeResult}. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * + * @category decoding + * @since 4.0.0 + */ +export const decodeUnknownResult = Parser.decodeUnknownResult + +/** + * 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 + * schema issue. + * + * **When to use** + * + * Use when typed input should be decoded into a `Result` with structured issue + * data. + * + * **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. + * + * @category decoding + * @since 4.0.0 + */ +export const decodeResult = Parser.decodeResult + +/** + * Decodes an `unknown` input against a schema, returning a `Promise` that + * resolves with the decoded value or rejects with a schema issue. + * + * **When to use** + * + * Use when integrating unknown input validation with Promise-based APIs. + * + * **Details** + * + * For typed input use {@link decodePromise}. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknownPromise = Parser.decodeUnknownPromise + +/** + * 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 + * schema issue. + * + * **When to use** + * + * Use when integrating typed input decoding with Promise-based APIs. + * + * **Details** + * + * For `unknown` input use `decodeUnknownPromise`. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * + * @category decoding + * @since 3.10.0 + */ +export const decodePromise = Parser.decodePromise + +/** + * Decodes an `unknown` input against a schema synchronously, returning the + * decoded value or throwing an `Error` whose cause contains the schema issue. + * + * **When to use** + * + * Use when validating unknown data at a boundary and treating schema mismatches + * as exceptions. + * + * **Details** + * + * For typed input use `decodeSync`. + * Only service-free schemas can be decoded synchronously. For non-throwing + * alternatives see `decodeUnknownOption`, `decodeUnknownExit`, or + * `decodeUnknownEffect`. Options may be provided either when creating the + * decoder or when applying it; application options override creation options. + * + * **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" + * ``` + * + * @category decoding + * @since 4.0.0 + */ +export const decodeUnknownSync = Parser.decodeUnknownSync + +/** + * Decodes a typed input (the schema's `Encoded` type) against a schema + * synchronously, returning the decoded value or throwing an `Error` whose cause + * contains the schema issue. + * + * **When to use** + * + * Use when typed input should be decoded synchronously and schema mismatches + * should throw. + * + * **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. + * + * @category decoding + * @since 4.0.0 + */ +export const decodeSync = Parser.decodeSync + +/** + * 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 encoding input whose type is not statically known. + * + * **Details** + * + * Prefer {@link encodeEffect} when the input 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" + * ``` + * + * @category encoding + * @since 4.0.0 + */ +export function encodeUnknownEffect(schema: S, options?: AST.ParseOptions) { + const parser = Parser.encodeUnknownEffect(schema, options) + return ( + input: unknown, + options?: AST.ParseOptions + ): Effect.Effect => { + return Effect.mapErrorEager(parser(input, options), (issue) => new SchemaError(issue)) + } +} + +/** + * 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 the input is already typed as the schema's `Type`. + * + * **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. + * + * @category encoding + * @since 4.0.0 + */ +export const encodeEffect: ( + schema: S, + options?: AST.ParseOptions +) => (input: S["Type"], options?: AST.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` with + * a {@link SchemaError}. + * + * **When to use** + * + * Use when the input type is not statically known and encoding should return an + * `Exit` instead of failing or throwing. + * + * **Details** + * + * Only usable with schemas that have no `EncodingServices` requirement. Prefer + * {@link encodeExit} when the input 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. + * + * @category encoding + * @since 4.0.0 + */ +export function encodeUnknownExit>(schema: S, options?: AST.ParseOptions) { + const parser = Parser.encodeUnknownExit(schema, options) + return (input: unknown, options?: AST.ParseOptions): Exit_.Exit => { + return Exit_.mapError(parser(input, options), (issue) => new SchemaError(issue)) + } +} + +/** + * 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` with a {@link SchemaError}. + * + * **When to use** + * + * Use when typed input should be encoded into an `Exit` instead of failing or + * throwing. + * + * **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. + * + * @category encoding + * @since 4.0.0 + */ +export const encodeExit: >( + schema: S, + options?: AST.ParseOptions +) => (input: S["Type"], options?: AST.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` on failure. + * + * **When to use** + * + * Use when the input type is not statically known and you only need to know + * whether encoding succeeded. + * + * **Details** + * + * Prefer this over {@link encodeUnknownExit} or {@link encodeUnknownEffect} + * when you don't need error details. For typed input use {@link encodeOption}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknownOption = Parser.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` on + * failure. + * + * **When to use** + * + * Use when typed input should be encoded and only success or failure matters. + * + * **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. + * + * @category encoding + * @since 3.10.0 + */ +export const encodeOption = Parser.encodeOption + +/** + * Encodes an `unknown` input against a schema, returning a `Result` that + * succeeds with the encoded value or fails with a schema issue. + * + * **When to use** + * + * Use when the input type is not statically known and encoding should return a + * `Result` with structured issue data. + * + * **Details** + * + * For typed input use {@link encodeResult}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * @category encoding + * @since 4.0.0 + */ +export const encodeUnknownResult = Parser.encodeUnknownResult + +/** + * Encodes a typed input (the schema's `Type`) against a schema, returning a + * `Result` that succeeds with the encoded value or fails with a schema issue. + * + * **When to use** + * + * Use when typed input should be encoded into a `Result` with structured issue + * data. + * + * **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. + * + * @category encoding + * @since 4.0.0 + */ +export const encodeResult = Parser.encodeResult + +/** + * Encodes an `unknown` input against a schema, returning a `Promise` that + * resolves with the encoded value or rejects with a schema issue. + * + * **When to use** + * + * Use when integrating unknown input serialization with Promise-based APIs. + * + * **Details** + * + * For typed input use {@link encodePromise}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknownPromise = Parser.encodeUnknownPromise + +/** + * 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}. + * + * **When to use** + * + * Use when integrating typed input serialization with Promise-based APIs. + * + * **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. + * + * @category encoding + * @since 3.10.0 + */ +export const encodePromise = Parser.encodePromise + +/** + * Encodes an `unknown` input against a schema synchronously, throwing a + * {@link SchemaError} on failure. + * + * **When to use** + * + * Use when serializing unknown data at a boundary and treating schema + * mismatches as unrecoverable errors. + * + * **Details** + * + * For non-throwing alternatives see {@link encodeUnknownOption}, + * {@link encodeUnknownExit}, or {@link encodeUnknownEffect}. For typed input + * use {@link encodeSync}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * @category encoding + * @since 4.0.0 + */ +export const encodeUnknownSync = Parser.encodeUnknownSync + +/** + * Encodes a typed input (the schema's `Type`) against a schema synchronously, + * throwing a {@link SchemaError} on failure. + * + * **When to use** + * + * Use when typed input should be encoded synchronously and schema mismatches + * should throw. + * + * **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. + * + * @category encoding + * @since 4.0.0 + */ +export const encodeSync = Parser.encodeSync + +/** + * 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 +} + +/** + * Companion type for an exact optional struct key. The key may be absent, but + * when present must match the wrapped schema (no implicit `undefined`). + * Produced by {@link optionalKey}. + * + * @category models + * @since 4.0.0 + */ +export interface optionalKey extends + Bottom< + S["Type"], + S["Encoded"], + S["DecodingServices"], + S["EncodingServices"], + S["ast"], + optionalKey, + S["~type.make.in"], + S["Iso"], + S["~type.parameters"], + S["~type.make"], + S["~type.mutability"], + "optional", + S["~type.constructor.default"], + S["~encoded.mutability"], + "optional" + > +{ + readonly schema: S +} + +interface optionalKeyLambda extends Lambda { + (self: S): optionalKey + readonly "~lambda.out": this["~lambda.in"] extends Top ? 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(AST.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) + +/** + * Companion type for an optional struct key that also accepts `undefined`. + * Equivalent to `optionalKey>`. Produced 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 Top ? 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** (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]) + +/** + * Companion type for a mutable struct key. The key's property is writable. + * Produced by {@link mutableKey}. + * + * @category models + * @since 4.0.0 + */ +export interface mutableKey extends + Bottom< + S["Type"], + S["Encoded"], + S["DecodingServices"], + S["EncodingServices"], + S["ast"], + mutableKey, + S["~type.make.in"], + S["Iso"], + S["~type.parameters"], + S["~type.make"], + "mutable", + S["~type.optionality"], + S["~type.constructor.default"], + "mutable", + S["~encoded.optionality"] + > +{ + readonly schema: S +} + +interface mutableKeyLambda extends Lambda { + (self: S): mutableKey + readonly "~lambda.out": this["~lambda.in"] extends Top ? 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(AST.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) + +/** + * Schema type that collapses a transformation schema to its decoded `Type` on + * both sides (Type = Encoded = S["Type"]). Produced by {@link toType}. + * + * @category transforming + * @since 4.0.0 + */ +export interface toType extends + Bottom< + S["Type"], + S["Type"], + never, + never, + S["ast"], + toType, + 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"] + > +{} + +interface toTypeLambda extends Lambda { + (self: S): toType + readonly "~lambda.out": this["~lambda.in"] extends Top ? 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(AST.toType(schema.ast), { schema })) + +/** + * Schema type that collapses a transformation schema to its `Encoded` side on + * both sides (Type = Encoded = S["Encoded"]). Produced by {@link toEncoded}. + * + * @category transforming + * @since 4.0.0 + */ +export interface toEncoded extends + Bottom< + S["Encoded"], + S["Encoded"], + never, + never, + AST.AST, + toEncoded, + S["Encoded"], + S["Encoded"], + ReadonlyArray, + S["Encoded"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{} + +interface toEncodedLambda extends Lambda { + (self: S): toEncoded + readonly "~lambda.out": this["~lambda.in"] extends Top ? 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(AST.toEncoded(schema.ast), { schema })) + +const FlipTypeId = "~effect/Schema/flip" + +/** + * Schema type representing a flipped schema where `Type` and `Encoded` are + * swapped. Produced by {@link flip}. + * + * @category transforming + * @since 4.0.0 + */ +export interface flip extends + Bottom< + S["Encoded"], + S["Type"], + S["EncodingServices"], + S["DecodingServices"], + AST.AST, + flip, + S["Encoded"], + S["Encoded"], + ReadonlyArray, + S["Encoded"], + S["~encoded.mutability"], + S["~encoded.optionality"], + ConstructorDefault, + S["~type.mutability"], + S["~type.optionality"] + > +{ + 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** (Flip 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(AST.flip(schema.ast)) + } + return make(AST.flip(schema.ast), { [FlipTypeId]: FlipTypeId, schema }) +} + +/** + * Represents a schema for a single literal value. + * + * @see {@link Literal} for the constructor function. + * @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** (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 AST.Literal(literal), { + literal, + transform(to: L2): decodeTo, Literal> { + return out.pipe(decodeTo(Literal(to), { + decode: Getter.transform(() => to), + encode: Getter.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 Top { + 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 Codec ? `${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> + : `` +} + +/** + * Represents a schema that validates strings matching a template literal pattern. + * The encoded type is a string formed by concatenating the parts. + * + * @see {@link TemplateLiteral} for the constructor function. + * @category models + * @since 3.10.0 + */ +export interface TemplateLiteral extends + Bottom< + TemplateLiteral.Encoded, + TemplateLiteral.Encoded, + never, + never, + AST.TemplateLiteral, + TemplateLiteral + > +{ + readonly parts: Parts +} + +function templateLiteralFromParts(parts: Parts) { + return new AST.TemplateLiteral(parts.map((part) => isSchema(part) ? part.ast : new AST.Literal(part))) +} + +/** + * Creates a schema that validates strings matching a template literal pattern. Each part can be + * a literal string/number/bigint or a schema whose encoded type is a string, number, or bigint. + * + * **Example** (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 Codec ? T + : never, + ...Type + ] + : [] +} + +/** + * Represents a schema that validates strings matching a template literal pattern and decodes + * them into a tuple of typed values, one per schema part. + * + * @see {@link TemplateLiteralParser} for the constructor function. + * @category models + * @since 3.10.0 + */ +export interface TemplateLiteralParser extends + Bottom< + TemplateLiteralParser.Type, + TemplateLiteral.Encoded, + never, + never, + AST.Arrays, + TemplateLiteralParser + > +{ + readonly parts: Parts +} + +/** + * Schema for parsing template literal matches 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. + * + * **Example** (Parse 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 }) +} + +/** + * Represents a schema derived from a TypeScript enum object or an enum-like + * `as const` object, accepting any of its values. + * + * @see {@link Enum} for the constructor function. + * @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** (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 AST.Enum( + Object.keys(enums).filter( + (key) => typeof enums[enums[key]] !== "number" + ).map((key) => [key, enums[key]]) + ), + { enums } + ) +} + +/** + * Schema for the `never` type. Always fails validation. + * + * @see {@link Never} for the schema value. + * @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(AST.never) + +/** + * Schema for the `any` type. Accepts any value without validation. + * + * @see {@link Any} for the schema value. + * @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(AST.any) + +/** + * Schema for the `unknown` type. Accepts any value without validation. + * + * @see {@link Unknown} for the schema value. + * @category models + * @since 3.10.0 + */ +export interface Unknown extends Bottom {} + +/** + * Schema for the `unknown` type. Accepts any value without validation. + * + * @see {@link Any} for the `any` variant. + * @category schemas + * @since 3.10.0 + */ +export const Unknown: Unknown = make(AST.unknown) + +/** + * Schema for the `null` literal. Validates that the input is strictly `null`. + * + * @see {@link Null} for the schema value. + * @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(AST.null) + +/** + * Schema for the `undefined` literal. Validates that the input is strictly `undefined`. + * + * @see {@link Undefined} for the schema value. + * @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(AST.undefined) + +/** + * Schema for `string` values. + * + * @see {@link String} for the schema value. + * @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(AST.string) + +/** + * Schema for `number` values, including `NaN`, `Infinity`, and `-Infinity`. + * + * @see {@link Number} for the schema value. + * @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(AST.number) + +/** + * Schema for `boolean` values. + * + * @see {@link Boolean} for the schema value. + * @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(AST.boolean) + +/** + * Schema for `symbol` values. + * + * @see {@link Symbol} for the schema value. + * @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(AST.symbol) + +/** + * Schema for `bigint` values. + * + * @see {@link BigInt} for the schema value. + * @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(AST.bigInt) + +/** + * Schema for the `void` type. + * + * @see {@link Void} for the schema value. + * @category models + * @since 3.10.0 + */ +export interface Void extends Bottom {} + +/** + * Schema for the `void` type. Accepts `undefined` as the encoded value. + * + * @category schemas + * @since 3.10.0 + */ +export const Void: Void = make(AST.void) + +/** + * Schema for the `object` type keyword. + * + * @see {@link ObjectKeyword} for the schema value. + * @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(AST.objectKeyword) + +/** + * Represents a schema for a specific unique symbol. + * + * @see {@link UniqueSymbol} for the constructor function. + * @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** (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 AST.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]: Top } + + 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 Type_< + F extends Fields, + O extends keyof F = TypeOptionalKeys, + M extends keyof F = TypeMutableKeys + > = + & { readonly [K in keyof F as K extends M | O ? never : K]: F[K]["Type"] } + & { readonly [K in keyof F as K extends O ? K extends M ? never : K : never]?: F[K]["Type"] } + & { -readonly [K in keyof F as K extends M ? K extends O ? never : K : never]: F[K]["Type"] } + & { -readonly [K in keyof F as K extends M & O ? K : never]?: F[K]["Type"] } + + /** + * 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 = Simplify> + + type Iso_< + F extends Fields, + O extends keyof F = TypeOptionalKeys, + M extends keyof F = TypeMutableKeys + > = + & { readonly [K in keyof F as K extends M | O ? never : K]: F[K]["Iso"] } + & { readonly [K in keyof F as K extends O ? K extends M ? never : K : never]?: F[K]["Iso"] } + & { -readonly [K in keyof F as K extends M ? K extends O ? never : K : never]: F[K]["Iso"] } + & { -readonly [K in keyof F as K extends M & O ? K : never]?: F[K]["Iso"] } + + /** + * 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 = Simplify> + + 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 Encoded_< + F extends Fields, + O extends keyof F = EncodedOptionalKeys, + M extends keyof F = EncodedMutableKeys + > = + & { readonly [K in keyof F as K extends M | O ? never : K]: F[K]["Encoded"] } + & { readonly [K in keyof F as K extends O ? K extends M ? never : K : never]?: F[K]["Encoded"] } + & { -readonly [K in keyof F as K extends M ? K extends O ? never : K : never]: F[K]["Encoded"] } + & { -readonly [K in keyof F as K extends M & O ? K : never]?: F[K]["Encoded"] } + + /** + * 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 = Simplify> + + /** + * 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 MakeIn_< + F extends Fields, + O = TypeOptionalKeys | TypeConstructorDefaultedKeys + > = + & { readonly [K in keyof F as K extends O ? never : K]: F[K]["~type.make"] } + & { readonly [K in keyof F as K extends O ? K : never]?: F[K]["~type.make"] } + + /** + * 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 = Simplify> +} + +/** + * Schema type returned by `Schema.Struct` for an object with a fixed set of + * schema-defined fields. + * + * **Details** + * + * The `fields` property exposes the original field map for reuse, and + * `mapFields` creates a new struct schema by transforming that field map. + * + * @category models + * @since 3.10.0 + */ +export interface Struct extends + Bottom< + Struct.Type, + Struct.Encoded, + Struct.DecodingServices, + Struct.EncodingServices, + AST.Objects, + Struct, + Struct.MakeIn, + 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: AST.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(AST.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** (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(AST.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 to 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))) +} + +/** + * Schema type for a struct with renamed encoded keys. Produced by + * {@link encodeKeys}. + * + * @category transforming + * @since 4.0.0 + */ +export interface encodeKeys< + S extends Top & { 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 + } + > + > +{} + +/** + * 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. + * + * **Example** (Rename `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 Top & { 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 reverseMapping: any = {} + for (const k in self.fields) { + const encoded = toEncoded(self.fields[k]) + if (Object.hasOwn(mapping, k)) { + fields[mapping[k]!] = encoded + reverseMapping[mapping[k]!] = k + } else { + fields[k] = encoded + } + } + return Struct(fields).pipe(decodeTo( + self, + Transformation.transform({ + decode: Struct_.renameKeys(reverseMapping), + encode: Struct_.renameKeys(mapping) + }) + )) 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** (Add 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, + Transformation.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"] } +} + +/** + * Companion type for a key-value record (map) with a typed key and value schema. + * Produced by {@link Record}. + * + * **When to use** + * + * Use as the concrete schema type returned by `Record` when an API needs to + * accept or return a record schema with typed property keys and values. + * + * @see {@link Record} for constructing record schemas + * @see {@link StructWithRest} for combining fixed struct fields with record index signatures + * + * @category models + * @since 4.0.0 + */ +export interface $Record extends + Bottom< + Record.Type, + Record.Encoded, + Record.DecodingServices, + Record.EncodingServices, + AST.Objects, + $Record, + Simplify>, + Record.Iso + > +{ + readonly key: Key + readonly value: Value +} + +/** + * Defines a record (dictionary) schema with typed keys and values. + * + * **Example** (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 AST.KeyValueCombiner(options.keyValueCombiner.decode, options.keyValueCombiner.encode) + : undefined + return make(AST.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 = Top & { readonly ast: AST.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 + : {} + + /** + * 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 = + & S["Type"] + & MergeTuple<{ readonly [K in keyof Records]: Records[K]["Type"] }> + + /** + * 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 = + & S["Iso"] + & MergeTuple<{ readonly [K in keyof Records]: Records[K]["Iso"] }> + + /** + * 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 = + & S["Encoded"] + & MergeTuple<{ readonly [K in keyof Records]: Records[K]["Encoded"] }> + + /** + * 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 = + | S["DecodingServices"] + | { [K in keyof Records]: Records[K]["DecodingServices"] }[number] + + /** + * 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 = + | S["EncodingServices"] + | { [K in keyof Records]: Records[K]["EncodingServices"] }[number] + + /** + * 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 = + & S["~type.make"] + & MergeTuple<{ readonly [K in keyof Records]: Records[K]["~type.make"] }> +} + +/** + * Companion type for a struct combined with one or more record schemas. Produced + * by {@link StructWithRest}. + * + * **When to use** + * + * Use as the schema type when generic code needs to retain the base struct + * schema and all rest record schemas. + * + * @see {@link StructWithRest} for constructing this schema type + * @see {@link Record} for constructing record schemas used as rest index signatures + * + * @category models + * @since 4.0.0 + */ +export interface StructWithRest< + S extends StructWithRest.Objects, + Records extends StructWithRest.Records +> extends + Bottom< + Simplify>, + Simplify>, + StructWithRest.DecodingServices, + StructWithRest.EncodingServices, + AST.Objects, + StructWithRest, + Simplify>, + 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. + * + * **Example** (Struct with string-indexed extra keys) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.StructWithRest( + * Schema.Struct({ id: Schema.Number }), + * [Schema.Record(Schema.String, Schema.String)] + * ) + * + * // { readonly id: number } & { readonly [x: string]: string } + * 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(AST.structWithRest(schema.ast, records.map(AST.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_ +} + +/** + * Companion type for a fixed-length tuple. Produced by {@link Tuple}. + * + * @category models + * @since 3.10.0 + */ +export interface Tuple extends + Bottom< + Tuple.Type, + Tuple.Encoded, + Tuple.DecodingServices, + Tuple.EncodingServices, + AST.Arrays, + Tuple, + Tuple.MakeIn, + 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: AST.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(AST.tuple(elements, options?.unsafePreserveChecks ? this.ast.checks : undefined), elements) + } + }) +} + +/** + * Defines a fixed-length tuple schema from an array of element schemas. + * + * **Example** (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(AST.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 = Top & { + readonly Type: ReadonlyArray + readonly Encoded: ReadonlyArray + readonly ast: AST.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 [Top, ...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 Top, ...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 Top, ...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 Top, ...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 Top, ...infer Tail extends ReadonlyArray] ? readonly [ + ...M, + ...Array, + ...{ readonly [K in keyof Tail]: Tail[K]["~type.make"] } + ] : + M +} + +/** + * Companion type for a tuple with additional rest elements. Produced by + * {@link TupleWithRest}. + * + * @category models + * @since 4.0.0 + */ +export interface TupleWithRest< + S extends TupleWithRest.TupleType, + Rest extends TupleWithRest.Rest +> extends + Bottom< + TupleWithRest.Type, + TupleWithRest.Encoded, + S["DecodingServices"] | Rest[number]["DecodingServices"], + S["EncodingServices"] | Rest[number]["EncodingServices"], + AST.Arrays, + TupleWithRest, + TupleWithRest.MakeIn, + 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** (Tuple with rest) + * + * ```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(AST.tupleWithRest(schema.ast, rest.map(AST.getAST)), { schema, rest }) +} + +/** + * Schema interface produced by `Schema.Array` for readonly arrays. + * + * **When to use** + * + * Use as the public companion type returned by `Schema.Array` when you need to + * refer to a readonly array schema built from an element schema. + * + * **Details** + * + * The decoded type is `ReadonlyArray`, the encoded type is + * `ReadonlyArray`, and the element schema is available as + * `value`. + * + * @category models + * @since 4.0.0 + */ +export interface $Array extends + Bottom< + ReadonlyArray, + ReadonlyArray, + S["DecodingServices"], + S["EncodingServices"], + AST.Arrays, + $Array, + ReadonlyArray, + ReadonlyArray + > +{ + readonly value: S +} + +interface ArrayLambda extends Lambda { + (self: S): $Array + readonly "~lambda.out": this["~lambda.in"] extends Top ? $Array : never +} + +/** + * @category constructors + * @since 4.0.0 + */ +const ArraySchema = Struct_.lambda((schema) => + make(new AST.Arrays(false, [], [schema.ast]), { value: schema }) +) + +export { + /** + * Defines a `ReadonlyArray` schema for a given element schema. + * + * **Example** (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 +} + +/** + * Companion type for a non-empty `ReadonlyArray`. Produced by {@link NonEmptyArray}. + * + * **Details** + * + * The decoded type is `readonly [S["Type"], ...Array]`, the encoded type is + * `readonly [S["Encoded"], ...Array]`, and the element schema is available as + * `value`. + * + * @category models + * @since 3.10.0 + */ +export interface NonEmptyArray extends + Bottom< + readonly [S["Type"], ...Array], + readonly [S["Encoded"], ...Array], + S["DecodingServices"], + S["EncodingServices"], + AST.Arrays, + NonEmptyArray, + readonly [S["~type.make"], ...Array], + readonly [S["Iso"], ...Array] + > +{ + readonly value: S +} + +interface NonEmptyArrayLambda extends Lambda { + (self: S): NonEmptyArray + readonly "~lambda.out": this["~lambda.in"] extends Top ? NonEmptyArray : never +} + +/** + * Defines a non-empty `ReadonlyArray` schema — at least one element required. + * Type is `readonly [T, ...T[]]`. + * + * **Example** (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 AST.Arrays(false, [schema.ast], [schema.ast]), { value: schema }) +) + +/** + * Schema interface returned by `ArrayEnsure`, which normalizes a single item or + * an array of items into a readonly array. + * + * **When to use** + * + * Use as the schema type when generic code needs to retain the original item + * schema. + * + * **Details** + * + * The schema decodes from `S` or `Schema.Array(S)` and produces + * `ReadonlyArray`. + * + * @see {@link ArrayEnsure} for constructing this schema type + * + * @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)), + Transformation.transform({ + decode: Arr.ensure, + encode: (array) => array.length === 1 ? array[0] : array + }) + )) +} + +/** + * Companion type for an array with unique elements. Produced 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()) +} + +/** + * Schema type that makes array or tuple elements mutable (removes `readonly`). + * Produced by {@link mutable}. + * + * @category transforming + * @since 3.10.0 + */ +export interface mutable extends + Bottom< + Mutable, + Mutable, + S["DecodingServices"], + S["EncodingServices"], + S["ast"], + mutable, + // "~type.make" and "~type.make.in" as they are because they are contravariant + 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"] + > +{ + readonly schema: S +} + +interface mutableLambda extends Lambda { + (self: S): mutable + readonly "~lambda.out": this["~lambda.in"] extends Top & { readonly "ast": AST.Arrays } ? mutable + : "Error: schema not eligible for mutable" +} + +/** + * Makes an array or tuple schema mutable, removing the `readonly` modifier. + * + * **Example** (Mutable array) + * + * ```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 AST.Arrays(true, schema.ast.elements, schema.ast.rest), { schema }) +}) + +/** + * Companion type for a union of multiple schemas. Produced by {@link Union}. + * + * @category models + * @since 3.10.0 + */ +export interface Union> extends + Bottom< + { [K in keyof Members]: Members[K]["Type"] }[number], + { [K in keyof Members]: Members[K]["Encoded"] }[number], + { [K in keyof Members]: Members[K]["DecodingServices"] }[number], + { [K in keyof Members]: Members[K]["EncodingServices"] }[number], + AST.Union<{ [K in keyof Members]: Members[K]["ast"] }[number]>, + Union, + { [K in keyof Members]: Members[K]["~type.make"] }[number], + { [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: AST.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( + AST.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** (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(AST.union(members, options?.mode ?? "anyOf", undefined), members) +} + +/** + * Represents a union schema of multiple literal values. + * + * @see {@link Literals} for the constructor function. + * @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** (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(AST.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 + } + }) +} + +/** + * Companion type for `S | null`. Produced 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 Top ? NullOr : never +} + +/** + * Creates a union schema of `S | null`. + * + * @category constructors + * @since 3.10.0 + */ +export const NullOr = Struct_.lambda((self) => Union([self, Null])) + +/** + * Companion type for `S | undefined`. Produced 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 Top ? UndefinedOr : never +} + +/** + * Creates a union schema of `S | undefined`. + * + * @category constructors + * @since 3.10.0 + */ +export const UndefinedOr = Struct_.lambda((self) => Union([self, Undefined])) + +/** + * Companion type for `S | null | undefined`. Produced 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 Top ? 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])) + +/** + * Schema type wrapping a lazily-evaluated schema. Produced by {@link suspend}. + * + * @category models + * @since 3.10.0 + */ +export interface suspend extends + Bottom< + S["Type"], + S["Encoded"], + S["DecodingServices"], + S["EncodingServices"], + AST.Suspend, + suspend, + 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"] + > +{} + +/** + * 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** (Recursive tree schema) + * + * ```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 AST.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 [AST.Check, ...Array>]) { + return (self: S): S["Rebuild"] => self.check(...checks) +} + +/** + * The output type of {@link refine}, narrowing the schema's `Type` to `T` via a + * type guard. + * + * @category filtering + * @since 3.10.0 + */ +export interface refine extends + Bottom< + T, + S["Encoded"], + S["DecodingServices"], + S["EncodingServices"], + S["ast"], + refine, + S["~type.make.in"], + T, + S["~type.parameters"], + T, + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + 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(AST.appendChecks(schema.ast, [AST.makeFilterByGuard(refinement, annotations)]), { schema }) +} + +type DistributeBrands = UnionToIntersection : never> + +/** + * The output type of {@link brand}, intersecting the schema's `Type` with one or + * more {@link Brand.Brand} tags. + * + * **When to use** + * + * Use as the schema type when generic code needs to retain the wrapped schema + * and nominal brand type. + * + * @see {@link brand} for adding the brand tag to an existing schema + * + * @category branding + * @since 3.10.0 + */ +export interface brand extends + Bottom< + S["Type"] & DistributeBrands, + S["Encoded"], + S["DecodingServices"], + S["EncodingServices"], + S["ast"], + brand, + S["~type.make.in"], + S["Type"] & DistributeBrands, + S["~type.parameters"], + S["Type"] & DistributeBrands, + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + 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(AST.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)) + } +} + +/** + * A schema that wraps another schema and intercepts its decoding pipeline. + * + * **Details** + * + * The interceptor receives the full decoding `Effect` and may replace, modify, + * or augment it — including adding service requirements via `RD`. + * + * @see {@link middlewareDecoding} for the constructor + * @category decoding + * @since 4.0.0 + */ +export interface middlewareDecoding extends + Bottom< + S["Type"], + S["Encoded"], + RD, + S["EncodingServices"], + S["ast"], + middlewareDecoding, + 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"] + > +{ + 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, Issue.Issue, S["DecodingServices"]>, + options: AST.ParseOptions + ) => Effect.Effect, Issue.Issue, RD> +) { + return (schema: S): middlewareDecoding => + make( + AST.middlewareDecoding(schema.ast, new Transformation.Middleware(decode, identity)), + { schema } + ) +} + +/** + * A schema that wraps another schema and intercepts its encoding pipeline. + * + * **Details** + * + * The interceptor receives the full encoding `Effect` and may replace, modify, + * or augment it — including adding service requirements via `RE`. + * + * @see {@link middlewareEncoding} for the constructor + * @category encoding + * @since 4.0.0 + */ +export interface middlewareEncoding extends + Bottom< + S["Type"], + S["Encoded"], + S["DecodingServices"], + RE, + S["ast"], + middlewareEncoding, + 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"] + > +{ + 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, Issue.Issue, S["EncodingServices"]>, + options: AST.ParseOptions + ) => Effect.Effect, Issue.Issue, RE> +) { + return (schema: S): middlewareEncoding => + make( + AST.middlewareEncoding(schema.ast, new Transformation.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: Issue.Issue) => Effect.Effect, Issue.Issue> +): (self: S) => S["Rebuild"] { + return catchDecodingWithContext(f) +} + +/** + * Recovers from a decoding error with a handler that may require Effect services. + * + * **When to use** + * + * Use when decoding fallback logic needs 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: Issue.Issue) => Effect.Effect, Issue.Issue, R> +) { + return (self: S): middlewareDecoding => + self.pipe(middlewareDecoding(Effect.catchEager(f))) +} + +/** + * 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: Issue.Issue) => Effect.Effect, Issue.Issue> +): (self: S) => S["Rebuild"] { + return catchEncodingWithContext(f) +} + +/** + * Recovers from an encoding error with a handler that may require Effect services. + * + * **When to use** + * + * Use when encoding fallback logic needs 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: Issue.Issue) => Effect.Effect, Issue.Issue, R> +) { + return (self: S): middlewareEncoding => + self.pipe(middlewareEncoding(Effect.catchEager(f))) +} + +/** + * Schema type produced by `decodeTo` when a custom transformation composes a + * `From` schema with a `To` schema. + * + * **Details** + * + * `Type` is `To["Type"]` and `Encoded` is `From["Encoded"]`. Decoding services + * are `To["DecodingServices"] | From["DecodingServices"] | RD`; encoding + * services are `To["EncodingServices"] | From["EncodingServices"] | RE`. + * + * @see {@link compose} for the passthrough (no transformation) variant + * @category transforming + * @since 4.0.0 + */ +export interface decodeTo extends + Bottom< + To["Type"], + From["Encoded"], + To["DecodingServices"] | From["DecodingServices"] | RD, + To["EncodingServices"] | From["EncodingServices"] | RE, + To["ast"], + decodeTo, + To["~type.make.in"], + To["Iso"], + To["~type.parameters"], + To["~type.make"], + To["~type.mutability"], + To["~type.optionality"], + To["~type.constructor.default"], + From["~encoded.mutability"], + From["~encoded.optionality"] + > +{ + readonly from: From + readonly to: To +} + +/** + * The type produced by {@link decodeTo} when called without a custom transformation (passthrough composition). + * + * **Details** + * + * Equivalent to {@link decodeTo} with `RD = never` and `RE = never`, meaning the schemas + * are composed using their natural encoding/decoding chain. + * + * @see {@link decodeTo} for the transformation variant + * @category transforming + * @since 3.10.0 + */ +export interface compose extends decodeTo {} + +/** + * Creates a schema that transforms from a source schema to a target schema. + * + * **Details** + * + * This is a curried function: call it with the target schema `to` (and optionally a transformation), + * then call the returned function with the source schema `from`. The resulting schema decodes from + * `From["Encoded"]` to `To["Type"]` and encodes from `To["Type"]` back to `From["Encoded"]`. + * + * Key guarantees: + * - Resulting schema has `Type = To["Type"]` and `Encoded = From["Encoded"]` + * - When `transformation` is omitted, uses `Transformation.passthrough()` (schema composition) + * - Combines decoding/encoding services from both `from` and `to` schemas + * - Transformation `decode` maps `From["Type"]` → `To["Encoded"]` (used during encoding) + * - Transformation `encode` maps `To["Encoded"]` → `From["Type"]` (used during decoding) + * + * Common mistakes: + * - **Direction confusion**: Remember `to` is the target (what you decode TO), `from` is the source (what you decode FROM) + * - **Currying**: This is curried - must use pipe: `from.pipe(Schema.decodeTo(to))` + * - **Transformation direction**: `decode` goes `From["Type"]` → `To["Encoded"]`, `encode` goes `To["Encoded"]` → `From["Type"]` + * - **Passthrough assumption**: Without transformation, schemas must satisfy `To["Encoded"] === From["Type"]` or use passthrough helpers + * - **Service dependencies**: Resulting schema requires services from both schemas; use `Schema.provideService` if needed + * + * **Example** (String to Number with 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: Getter.Getter, NoInfer, RD> + readonly encode: Getter.Getter, NoInfer, RE> + } +): (from: From) => decodeTo +export function decodeTo( + to: To, + transformation?: { + readonly decode: Getter.Getter + readonly encode: Getter.Getter + } | undefined +) { + return (from: From) => { + return make( + AST.decodeTo( + from.ast, + to.ast, + transformation ? Transformation.make(transformation) : Transformation.passthrough() + ), + { + from, + to + } + ) + } +} + +/** + * Applies a transformation to a schema, creating a new schema with the same type but transformed encoding/decoding. + * + * **Details** + * + * This is a curried function: call it with a transformation object, then call the returned function with a schema. + * The resulting schema has `Type = S["Type"]` and `Encoded = S["Encoded"]`, with the transformation applied during + * encoding and decoding operations. + * + * Key guarantees: + * - Resulting schema has `Type = S["Type"]` and `Encoded = S["Encoded"]` + * - Uses `toType(self)` as the target schema internally (creates a schema where both Type and Encoded are `S["Type"]`) + * - Combines decoding/encoding services from the source schema and transformation + * - Transformation `decode` maps `S["Type"]` → `S["Type"]` (used during encoding) + * - Transformation `encode` maps `S["Type"]` → `S["Type"]` (used during decoding) + * + * Common mistakes: + * - **Currying**: This is curried - must use pipe: `schema.pipe(Schema.decode(transformation))` + * - **Transformation direction**: `decode` and `encode` both operate on `S["Type"]` (same type, different values) + * - **Service dependencies**: Resulting schema requires services from the source schema and transformation; use `Schema.provideService` if needed + * + * **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: Getter.Getter + readonly encode: Getter.Getter +}) { + return (self: S): decodeTo, S, RD, RE> => { + return self.pipe(decodeTo(toType(self), transformation)) + } +} + +/** + * 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** (Encode a number back to 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: Getter.Getter, NoInfer, RD> + readonly encode: Getter.Getter, NoInfer, RE> + } +): (from: From) => decodeTo +export function encodeTo( + to: To, + transformation?: { + readonly decode: Getter.Getter + readonly encode: Getter.Getter + } +) { + return (from: From): decodeTo => { + return transformation ? + to.pipe(decodeTo(from, transformation)) : + to.pipe(decodeTo(from)) + } +} + +/** + * 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: Getter.Getter + readonly encode: Getter.Getter +}) { + return (self: S): decodeTo, RD, RE> => { + return toEncoded(self).pipe(decodeTo(self, transformation)) + } +} + +/** + * 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" +} + +/** + * Schema type returned by `withConstructorDefault` after attaching a default used + * by constructor helpers. + * + * **Details** + * + * The default affects `make` and related constructor helpers only; decoding and + * encoding still use the original schema behavior. The schema is marked as + * already having a constructor default so another constructor default cannot be + * added. + * + * @see {@link withConstructorDefault} for the constructor + * @category constructors + * @since 3.10.0 + */ +export interface withConstructorDefault extends + Bottom< + S["Type"], + S["Encoded"], + S["DecodingServices"], + S["EncodingServices"], + S["ast"], + withConstructorDefault, + S["~type.make.in"], + S["Iso"], + S["~type.parameters"], + S["~type.make"], + S["~type.mutability"], + S["~type.optionality"], + "with-default", + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + 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** (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(AST.withConstructorDefault(schema.ast, Effect.mapErrorEager(defaultValue, (e) => e.issue)), { schema }) +} + +/** + * The type produced by {@link withDecodingDefaultKey}: a schema whose `Encoded` + * side is `optionalKey` and that fills in a default `Encoded` value during decoding. + * + * @see {@link withDecodingDefaultKey} for the constructor + * @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 models + * @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** (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" ? Getter.omit() : Getter.passthrough() + return (self: S): withDecodingDefaultKey => { + return optionalKey(toEncoded(self)).pipe(decodeTo(self, { + decode: Getter.withDefault(Effect.mapErrorEager(defaultValue, (e) => e.issue)), + encode + })) + } +} + +/** + * The type produced by {@link withDecodingDefaultTypeKey}: a schema whose + * `Encoded` side is `optionalKey` and that fills in a default `Type` value + * during decoding. + * + * @see {@link withDecodingDefaultTypeKey} for the constructor + * @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)) + ) + } +} + +/** + * The type produced by {@link withDecodingDefault}: a schema whose `Encoded` + * side is `optional` and that fills in a default `Encoded` value during decoding. + * + * @see {@link withDecodingDefault} for the constructor + * @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. + * + * **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** (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" ? Getter.omit() : Getter.passthrough() + return (self: S): withDecodingDefault => { + return optional(toEncoded(self)).pipe(decodeTo(self, { + decode: Getter.withDefault(Effect.mapErrorEager(defaultValue, (e) => e.issue)), + encode + })) + } +} + +/** + * The type produced by {@link withDecodingDefaultType}: a schema whose + * `Encoded` side is `optional` and that fills in a default `Type` value during + * decoding. + * + * @see {@link withDecodingDefaultType} for the constructor + * @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. + * + * **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)) + ) + } +} + +/** + * The type produced by {@link tag} — a literal schema with a constructor default. + * + * **Details** + * + * Used as the type of the `_tag` field in {@link TaggedStruct} and related helpers. + * + * @see {@link tag} for the constructor + * @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** (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** (Tag omitted 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" })) +} + +/** + * The type produced by {@link TaggedStruct} — a {@link Struct} with an extra `_tag` field of type {@link tag}. + * + * @see {@link TaggedStruct} for the constructor + * @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** (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 + * 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 + } +} + +/** + * The type produced by {@link toTaggedUnion} — a {@link Union} augmented with `cases`, `guards`, `isAnyOf`, and `match` utilities. + * + * @see {@link toTaggedUnion} for the constructor + * @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: Top) { + const ast = schema.ast + + if ( + AST.isUnion(ast) && "members" in schema && globalThis.Array.isArray(schema.members) && + schema.members.every(isSchema) + ) { + return schema.members.forEach(walk) + } + + const sentinels = AST.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) + } + } +} + +/** + * A union schema that exposes `cases`, `guards`, `isAnyOf`, and `match` utilities keyed by the `_tag` discriminant. + * Produced by {@link TaggedUnion}. + * + * @see {@link TaggedUnion} for the constructor + * @category models + * @since 4.0.0 + */ +export interface TaggedUnion> extends + Bottom< + { [K in keyof Cases]: Cases[K]["Type"] }[keyof Cases], + { [K in keyof Cases]: Cases[K]["Encoded"] }[keyof Cases], + { [K in keyof Cases]: Cases[K]["DecodingServices"] }[keyof Cases], + { [K in keyof Cases]: Cases[K]["EncodingServices"] }[keyof Cases], + AST.Union, + TaggedUnion, + { [K in keyof Cases]: Cases[K]["~type.make"] }[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** (Discriminated union with pattern matching) + * + * ```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 }) +} + +/** + * The interface type for schemas created by {@link Opaque}. + * Carries the same encoded/decoded shape as `S` but replaces `Type` with `Self & Brand`, + * making the decoded value nominally distinct. + * + * @see {@link Opaque} for the constructor + * @category models + * @since 4.0.0 + */ +export interface Opaque extends + Bottom< + Self, + 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"] + > +{ + 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** (Opaque struct) + * + * ```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) + } +} + +/** + * The type produced by {@link instanceOf} — a declaration schema that validates class instances. + * + * @see {@link instanceOf} for the constructor + * @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** (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 `AST.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: Getter.Getter> + readonly encode: Getter.Getter, T> + } + ): AST.Link => { + return new AST.Link(encodeTo.ast, Transformation.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** (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 Checks Constructors + * @since 4.0.0 + */ +export const makeFilter: ( + filter: (input: T, ast: AST.AST, options: AST.ParseOptions) => FilterOutput, + annotations?: Annotations.Filter | undefined, + abort?: boolean +) => AST.Filter = AST.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 Issue.InvalidValue} wrapping the input, with the string used as + * the issue's `message` annotation. + * - {@link Issue.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 Issue.InvalidValue}) or a full + * {@link Issue.Issue}; the result is wrapped in an {@link Issue.Pointer} + * at the given `path`. + * + * @category models + * @since 3.10.0 + */ +export type FilterIssue = string | Issue.Issue | { + readonly path: ReadonlyArray + readonly issue: string | Issue.Issue +} + +/** + * The value a filter predicate (see {@link makeFilter}) may return. + * + * **Details** + * + * Each shape is normalized into an {@link Issue.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 Issue.InvalidValue} wrapping + * the input, with no custom message. + * - {@link FilterIssue}: a single failure. See {@link FilterIssue} for the + * shapes (`string`, {@link Issue.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 Issue.Composite}. + * + * @category models + * @since 3.10.0 + */ +export type FilterOutput = + | undefined + | boolean + | FilterIssue + | ReadonlyArray + +/** + * Groups multiple checks into a single {@link AST.FilterGroup}, applying + * optional shared annotations to the group as a whole. + * + * @category Checks Constructors + * @since 4.0.0 + */ +export function makeFilterGroup( + checks: readonly [AST.Check, ...Array>], + annotations: Annotations.Filter | undefined = undefined +): AST.FilterGroup { + return new AST.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) + }, + toArbitraryConstraint: { + string: { + 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) => AST.Filter = + AST.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) => AST.Filter = AST.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) => AST.Filter = AST.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) => AST.Filter = AST.isStringSymbol + +/** + * Returns a RegExp for validating an 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)$/ +} + +/** + * Validates that a string is a valid Universally Unique Identifier (UUID). + * Optionally specify a version (1-8) to validate against a specific UUID version. + * If no version is specified (`undefined`), all versions are supported. + * + * **Details** + * + * 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. + * + * @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 + } + ) +} + +/** + * 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}`) + }, + toArbitraryConstraint: { + string: { + 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}$`) + }, + toArbitraryConstraint: { + string: { + 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) + }, + toArbitraryConstraint: { + string: { + 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) + }, + toArbitraryConstraint: { + string: { + 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) + }, + toArbitraryConstraint: { + string: { + 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) + }, + toArbitraryConstraint: { + string: { + 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) + }, + toArbitraryConstraint: { + string: { + 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 `noDefaultInfinity` + * and `noNaN` 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" + }, + toArbitraryConstraint: { + number: { + noDefaultInfinity: 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)}`, + ...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)}`, + ...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)}`, + ...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)}`, + ...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)" : ""}`, + ...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 a `min` constraint + * with `minExcluded: true` 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 + }, + toArbitraryConstraint: { + number: { + min: exclusiveMinimum, + minExcluded: true + } + } + }) +}) + +/** + * 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 `min` 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 + }, + toArbitraryConstraint: { + number: { + min: 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 a `max` constraint + * with `maxExcluded: true` 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 + }, + toArbitraryConstraint: { + number: { + max: exclusiveMaximum, + maxExcluded: true + } + } + }) +}) + +/** + * 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 `max` 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 + }, + toArbitraryConstraint: { + number: { + max: 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 `min` and `max` + * constraints with optional `minExcluded` and `maxExcluded` 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 + }, + toArbitraryConstraint: { + number: { + min: options.minimum, + max: options.maximum, + ...(options.exclusiveMinimum && { minExcluded: true }), + ...(options.exclusiveMaximum && { maxExcluded: true }) + } + } + } + } +}) + +/** + * 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 `isInteger: 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" + }, + toArbitraryConstraint: { + number: { + isInteger: 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 AST.FilterGroup( + [ + isInt(annotations), + 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 AST.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 `noInvalidDate` + * 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" + }, + toArbitraryConstraint: { + date: { + noInvalidDate: true + } + }, + ...annotations + } + ) +} + +const nextDate = (date: globalThis.Date) => new globalThis.Date(date.getTime() + 1) + +const previousDate = (date: globalThis.Date) => new globalThis.Date(date.getTime() - 1) + +/** + * 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 + }, + toArbitraryConstraint: { + date: { + min: nextDate(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 + }, + toArbitraryConstraint: { + date: { + min: 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 + }, + toArbitraryConstraint: { + date: { + max: previousDate(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 + }, + toArbitraryConstraint: { + date: { + max: 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 + }, + toArbitraryConstraint: { + date: { + min: options.exclusiveMinimum ? nextDate(options.minimum) : options.minimum, + max: options.exclusiveMaximum ? previousDate(options.maximum) : options.maximum + } + } + }) +}) + +const nextBigInt = (n: bigint) => n + globalThis.BigInt(1) + +const previousBigInt = (n: bigint) => n - globalThis.BigInt(1) + +/** + * 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 + }, + toArbitraryConstraint: { + bigint: { + min: nextBigInt(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 + }, + toArbitraryConstraint: { + bigint: { + min: 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 + }, + toArbitraryConstraint: { + bigint: { + max: previousBigInt(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 + }, + toArbitraryConstraint: { + bigint: { + max: 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 + }, + toArbitraryConstraint: { + bigint: { + min: options.exclusiveMinimum ? nextBigInt(options.minimum) : options.minimum, + max: options.exclusiveMaximum ? previousBigInt(options.maximum) : options.maximum + } + } + }) +}) + +/** + * 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** (Minimum length check) + * + * ```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 + }, + [AST.STRUCTURAL_ANNOTATION_KEY]: true, + toArbitraryConstraint: { + string: { + minLength + }, + array: { + 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 + }, + [AST.STRUCTURAL_ANNOTATION_KEY]: true, + toArbitraryConstraint: { + string: { + maxLength + }, + array: { + 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 + }, + [AST.STRUCTURAL_ANNOTATION_KEY]: true, + toArbitraryConstraint: { + string: { + minLength: minimum, + maxLength: maximum + }, + array: { + 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 `minLength` + * constraint to the array representation to ensure generated values have at + * least the required size. + * + * @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 + }, + [AST.STRUCTURAL_ANNOTATION_KEY]: true, + toArbitraryConstraint: { + array: { + 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 `maxLength` + * constraint to the array representation to ensure generated values have at + * most the required size. + * + * @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 + }, + [AST.STRUCTURAL_ANNOTATION_KEY]: true, + toArbitraryConstraint: { + array: { + 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 `minLength` and + * `maxLength` constraints to ensure generated values have a size within the + * specified range. + * + * @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 + }, + [AST.STRUCTURAL_ANNOTATION_KEY]: true, + toArbitraryConstraint: { + array: { + 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 `minLength` + * constraint to the array of entries that is generated before being converted + * to an object, ensuring the resulting object has at least the required number + * of 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 + }, + [AST.STRUCTURAL_ANNOTATION_KEY]: true, + toArbitraryConstraint: { + array: { + 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 `maxLength` + * constraint to the array of entries that is generated before being converted + * to an object, ensuring the resulting object has at most the required number + * of 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 + }, + [AST.STRUCTURAL_ANNOTATION_KEY]: true, + toArbitraryConstraint: { + array: { + 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 `minLength` and + * `maxLength` constraints to the array of entries that is generated before + * being converted to an object. + * + * @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 + }, + [AST.STRUCTURAL_ANNOTATION_KEY]: true, + toArbitraryConstraint: { + array: { + 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: Top, annotations?: Annotations.Filter) { + const propertyNames = toEncoded(keySchema) + const parser = Parser._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 Issue.Pointer([key], issue)) + if (options.errors === "first") break + } + } + if (Arr.isArrayNonEmpty(issues)) { + return new Issue.Composite(ast, Option_.some(input), issues) + } + return true + }, + { + expected: "an object with property names matching the schema", + meta: { + _tag: "isPropertyNames", + propertyNames: propertyNames.ast + }, + [AST.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 comparator based on + * Effect equality to ensure generated arrays contain only unique items. + * + * @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" + }, + toArbitraryConstraint: { + array: { + comparator: equivalence + } + }, + ...annotations + } + ) +} + +// ----------------------------------------------------------------------------- +// Built-in Schemas +// ----------------------------------------------------------------------------- + +/** + * Type-level representation of the `NonEmptyString` schema, which validates + * strings with a length of at least one. + * + * @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 the `Char` schema, which validates strings whose + * length is exactly one. + * + * @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)) + +/** + * Schema for the `Option` type, representing an optional value that is + * either `None` or `Some`. + * + * **Example** (Option schema) + * + * ```ts + * import { Option, Schema } from "effect" + * + * const schema = Schema.Option(Schema.Number) + * + * Schema.decodeUnknownSync(schema)(Option.some(1)) + * // => Some(1) + * Schema.decodeUnknownSync(schema)(Option.none()) + * // => None + * ``` + * + * @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( + Parser.decodeUnknownEffect(value)(input.value, options), + { + onSuccess: Option_.some, + onFailure: (issue) => new Issue.Composite(ast, Option_.some(input), [new Issue.Pointer(["value"], issue)]) + } + ) + } + return Effect.fail(new Issue.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") }) + ]), + Transformation.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) => { + return fc.oneof( + ctx?.isSuspend ? { maxDepth: 2, depthIdentifier: "Option" } : {}, + fc.constant(Option_.none()), + value.map(Option_.some) + ) + }, + toEquivalence: ([value]) => Option_.makeEquivalence(value), + toFormatter: ([value]) => + Option_.match({ + onNone: () => "none()", + onSome: (t) => `some(${value(t)})` + }) + } + ) + return make(schema.ast, { value }) +} + +/** + * Type-level representation of a schema that decodes `null` as `None` and all + * other values as `Some`. + * + * @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: + * - `null` is decoded as `None` + * - other values are decoded as `Some` + * + * Encoding: + * - `None` is encoded as `null` + * - `Some` is encoded as the value + * + * @category Option + * @since 3.10.0 + */ +export function OptionFromNullOr(schema: S): OptionFromNullOr { + return NullOr(schema).pipe(decodeTo( + Option(toType(schema)), + Transformation.optionFromNullOr() + )) +} + +/** + * Type-level representation of a schema that decodes `undefined` as `None` and + * all other values as `Some`. + * + * @category Option + * @since 3.10.0 + */ +export interface OptionFromUndefinedOr extends decodeTo>, UndefinedOr> { + readonly "Rebuild": OptionFromUndefinedOr +} + +/** + * Decodes an undefined-or value `T` to a required `Option` value. + * + * **Details** + * + * Decoding: + * - `undefined` is decoded as `None` + * - other values are decoded as `Some` + * + * Encoding: + * - `None` is encoded as `undefined` + * - `Some` is encoded as the value + * + * @category Option + * @since 3.10.0 + */ +export function OptionFromUndefinedOr(schema: S): OptionFromUndefinedOr { + return UndefinedOr(schema).pipe(decodeTo( + Option(toType(schema)), + Transformation.optionFromUndefinedOr() + )) +} + +/** + * Type-level representation of a schema that decodes `null` or `undefined` as + * `None` and all other values as `Some`. + * + * @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: + * - `null` and `undefined` are decoded as `None` + * - other values are decoded as `Some` + * + * Encoding: + * - `None` is encoded as `null` or `undefined` depending on the provided `options.onNoneEncoding` (defaults to `undefined`) + * - `Some` is encoded as the 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)), + Transformation.optionFromNullishOr(options) + )) +} + +/** + * Type-level representation of a schema that decodes a missing object key as + * `None` and a present key as `Some`. + * + * @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: + * - a missing key is decoded as `None` + * - a present value is decoded as `Some` + * + * Encoding: + * - `None` is encoded as missing key + * - `Some` is encoded as the value + * + * @category Option + * @since 4.0.0 + */ +export function OptionFromOptionalKey(schema: S): OptionFromOptionalKey { + return optionalKey(schema).pipe(decodeTo( + Option(toType(schema)), + Transformation.optionFromOptionalKey() + )) +} + +/** + * Type-level representation of a schema that decodes a missing key or + * `undefined` value as `None` and other present values as `Some`. + * + * @category Option + * @since 4.0.0 + */ +export interface OptionFromOptional extends decodeTo>, optional> { + readonly "Rebuild": OptionFromOptional +} + +/** + * Decodes an optional or `undefined` value `A` to an required `Option` + * value. + * + * **Details** + * + * Decoding: + * - a missing key is decoded as `None` + * - a present key with an `undefined` value is decoded as `None` + * - all other values are decoded as `Some` + * + * Encoding: + * - `None` is encoded as missing key + * - `Some` is encoded as the value + * + * @category Option + * @since 4.0.0 + */ +export function OptionFromOptional(schema: S): OptionFromOptional { + return optional(schema).pipe(decodeTo( + Option(toType(schema)), + Transformation.optionFromOptional() + )) +} + +/** + * Type-level representation of a schema that decodes a missing key, `undefined`, + * or `null` as `None` and all other present values as `Some`. + * + * @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: + * - a missing key is decoded as `None` + * - a present key with an `undefined` value is decoded as `None` + * - a present key with a `null` value is decoded as `None` + * - all other values are decoded as `Some` + * + * Encoding (controlled by `options.onNoneEncoding`): + * - `"omit"` (default): `None` is encoded as a missing key + * - `null`: `None` is encoded as `null` + * - `undefined`: `None` is encoded as `undefined` + * - `Some` is always encoded as the value + * + * @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)), + Transformation.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)) + }) + )) +} + +/** + * Schema for the `Result` type, representing a computation that either + * succeeds with `A` or fails with `E`. + * + * @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 Issue.InvalidType(ast, Option_.some(input))) + } + switch (input._tag) { + case "Success": + return Effect.mapBothEager(Parser.decodeEffect(success)(input.success, options), { + onSuccess: Result_.succeed, + onFailure: (issue) => new Issue.Composite(ast, Option_.some(input), [new Issue.Pointer(["success"], issue)]) + }) + case "Failure": + return Effect.mapBothEager(Parser.decodeEffect(failure)(input.failure, options), { + onSuccess: Result_.fail, + onFailure: (issue) => new Issue.Composite(ast, Option_.some(input), [new Issue.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 }) + ]), + Transformation.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) => { + return fc.oneof( + ctx?.isSuspend ? { maxDepth: 2, depthIdentifier: "Result" } : {}, + success.map(Result_.succeed), + failure.map(Result_.fail) + ) + }, + 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 }) +} + +/** + * Schema for the `Redacted` type, providing secure handling of sensitive + * values. The inner value is hidden from error messages. + * + * @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. + * + * @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" + ? Parser.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 Issue.Pointer(["label"], issue) + ) + : Effect.void + return Effect.flatMapEager( + label, + () => + Effect.mapBothEager( + Parser.decodeUnknownEffect(value)(Redacted_.value(input), poptions), + { + onSuccess: () => input, + onFailure: (/** ignore the actual issue because of security reasons */) => { + const oinput = Option_.some(input) + return new Issue.Composite(ast, oinput, [ + new Issue.Pointer(["value"], new Issue.InvalidValue(oinput)) + ]) + } + } + ) + ) + } + return Effect.fail(new Issue.InvalidType(ast, Option_.some(input))) + }, + { + typeConstructor: { + _tag: "effect/Redacted" + }, + generation: { + runtime: `Schema.Redacted(?)`, + Type: `Redacted.Redacted`, + importDeclaration: `import * as Redacted from "effect/Redacted"` + }, + expected: "Redacted", + toCodecJson: ([value]) => + link>()( + redact(value), + { + decode: Getter.transform((e) => Redacted_.make(e, { label: options?.label })), + encode: options?.disallowJsonEncode ? + Getter.forbidden((oe) => + "Cannot serialize Redacted" + + (Option_.isSome(oe) && typeof oe.value.label === "string" ? ` with label: "${oe.value.label}"` : "") + ) : + Getter.transform(Redacted_.value) + } + ), + toArbitrary: ([value]) => () => value.map((a) => Redacted_.make(a, { label: options?.label })), + toFormatter: () => globalThis.String, + toEquivalence: ([value]) => Redacted_.makeEquivalence(value) + } + ) + return make(schema.ast, { value }) +} + +/** + * Type-level representation of a schema that decodes a raw value with the + * provided schema and wraps the result in `Redacted`. + * + * @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 schema.pipe(middlewareDecoding(Effect.mapErrorEager(Issue.redact))) +} + +/** + * 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. + * + * @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: Getter.transform((t) => Redacted_.make(t, { label: options?.label })), + encode: options?.disallowEncode ? + Getter.forbidden((oe) => + "Cannot encode Redacted" + + (Option_.isSome(oe) && typeof oe.value.label === "string" ? ` with label: "${oe.value.label}"` : "") + ) : + Getter.transform(Redacted_.value) + } + ) + ) +} + +/** + * Schema for a single `Cause.Reason`, representing one reason a fiber may fail: + * a typed error (`Fail`), an unexpected defect (`Die`), or an interrupt + * (`Interrupt`). + * + * **When to use** + * + * Use as the schema type when generic code needs to retain the typed failure + * and defect schemas for a single cause reason. + * + * **Details** + * + * The `error` schema validates typed failures and the `defect` schema validates + * unexpected defects. + * + * @see {@link CauseReason} for constructing this schema type + * @see {@link CauseReasonIso} for the ISO shape of each cause reason + * + * @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 to validate, transform, or serialize individual `Cause.Reason` values + * when typed failures and unexpected defects need separate schemas. + * + * **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 Issue.InvalidType(ast, Option_.some(input))) + } + switch (input._tag) { + case "Fail": + return Effect.mapBothEager( + Parser.decodeUnknownEffect(error)(input.error, options), + { + onSuccess: Cause_.makeFailReason, + onFailure: (issue) => new Issue.Composite(ast, Option_.some(input), [new Issue.Pointer(["error"], issue)]) + } + ) + case "Die": + return Effect.mapBothEager( + Parser.decodeUnknownEffect(defect)(input.defect, options), + { + onSuccess: Cause_.makeDieReason, + onFailure: (issue) => + new Issue.Composite(ast, Option_.some(input), [new Issue.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) }) + ]), + Transformation.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: FastCheck.Arbitrary, defect: FastCheck.Arbitrary) { + return (fc: typeof FastCheck, ctx: Annotations.ToArbitrary.Context | undefined) => { + return fc.oneof( + ctx?.isSuspend ? { maxDepth: 2, depthIdentifier: "Cause.Failure" } : {}, + fc.constant(Cause_.makeInterruptReason()), + fc.integer({ min: 1 }).map(Cause_.makeInterruptReason), + error.map((e) => Cause_.makeFailReason(e)), + defect.map((d) => Cause_.makeDieReason(d)) + ) + } +} + +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" + } + } +} + +/** + * Schema for `Cause` values, represented as an ordered collection of failure + * reasons combining typed errors, defects, and interrupts. + * + * **When to use** + * + * Use as the schema type when generic code needs to retain the typed failure + * and defect schemas for a full cause. + * + * **Details** + * + * The `error` schema validates typed failures and the `defect` schema validates + * unexpected defects. + * + * @see {@link Cause} for constructing this schema type + * @see {@link CauseIso} for the ordered array representation used by the schema ISO + * + * @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 Issue.InvalidType(ast, Option_.some(input))) + } + return Effect.mapBothEager(Parser.decodeUnknownEffect(failures)(input.reasons, options), { + onSuccess: Cause_.fromReasons, + onFailure: (issue) => new Issue.Composite(ast, Option_.some(input), [new Issue.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)), + Transformation.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: FastCheck.Arbitrary, defect: FastCheck.Arbitrary) { + return (fc: typeof FastCheck, ctx: Annotations.ToArbitrary.Context | undefined) => { + return fc.array(causeReasonToArbitrary(error, defect)(fc, ctx)).map(Cause_.fromReasons) + } +} + +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 the schema for JavaScript `Error` instances. + * + * @category Error + * @since 4.0.0 + */ +export interface Error extends instanceOf { + readonly "Rebuild": Error +} + +const ErrorJsonEncoded = Struct({ + message: String, + name: optionalKey(String), + stack: optionalKey(String) +}) + +/** + * Schema for JavaScript `Error` objects. + * + * **Details** + * + * Default JSON serializer: + * Encodes an `Error` as an object with `message` and optional `name` properties, + * and decodes that object back into an `Error`. The stack trace is omitted from + * the encoded form for security. + * + * @category schemas + * @since 4.0.0 + */ +export const Error: Error = instanceOf(globalThis.Error, { + typeConstructor: { + _tag: "Error" + }, + generation: { + runtime: `Schema.Error`, + Type: `globalThis.Error` + }, + expected: "Error", + toCodecJson: () => link()(ErrorJsonEncoded, Transformation.errorFromErrorJsonEncoded()), + toArbitrary: () => (fc) => fc.string().map((message) => new globalThis.Error(message)) +}) + +/** + * Schema for JavaScript `Error` objects that preserves stack traces in the JSON + * encoded form. + * + * **Details** + * + * Default JSON serializer: + * Encodes an `Error` as an object with `message`, optional `name`, and optional + * `stack` properties, and decodes that object back into an `Error`. + * + * @category schemas + * @since 4.0.0 + */ +export const ErrorWithStack: Error = instanceOf(globalThis.Error, { + typeConstructor: { + _tag: "ErrorWithStack" + }, + generation: { + runtime: `Schema.ErrorWithStack`, + Type: `globalThis.Error` + }, + expected: "Error", + toCodecJson: () => + link()( + ErrorJsonEncoded, + Transformation.errorFromErrorJsonEncoded({ + includeStack: true + }) + ), + toArbitrary: () => (fc) => fc.string().map((message) => new globalThis.Error(message)) +}) + +/** + * Type-level representation of the `Defect` schema, which accepts JavaScript + * `Error` values and arbitrary unknown defect values. + * + * @category Defect + * @since 3.10.0 + */ +export interface Defect extends + Union< + readonly [ + decodeTo< + Error, + Struct<{ + readonly message: String + readonly name: optionalKey + readonly stack: optionalKey + }> + >, + decodeTo + ] + > +{ + readonly "Rebuild": Defect +} + +const defectTransformation = new Transformation.Transformation( + Getter.passthrough(), + Getter.transform((u) => { + try { + return JSON.parse(JSON.stringify(u)) + } catch { + return format(u) + } + }) +) + +/** + * Schema for defect values, accepting either JavaScript `Error` values encoded + * with `message` and optional `name`, or arbitrary unknown defect values. + * + * **Details** + * + * Default JSON serializer: + * Unknown defects are serialized with `JSON.stringify` when possible and fall + * back to Effect's formatted representation when JSON serialization fails. + * + * @category constructors + * @since 3.10.0 + */ +export const Defect: Defect = Union([ + ErrorJsonEncoded.pipe(decodeTo(Error, Transformation.errorFromErrorJsonEncoded())), + Any.pipe(decodeTo( + Unknown.annotate({ + toCodecJson: () => link()(Any, defectTransformation), + toArbitrary: () => (fc) => fc.json() + }), + defectTransformation + )) +]) + +/** + * Schema for defects that also includes stack traces in the encoded form. + * + * @category Defect + * @since 4.0.0 + */ +export const DefectWithStack: Defect = Union([ + ErrorJsonEncoded.pipe(decodeTo( + ErrorWithStack, + Transformation.errorFromErrorJsonEncoded({ + includeStack: true + }) + )), + Any.pipe(decodeTo( + Unknown.annotate({ + toCodecJson: () => link()(Any, defectTransformation), + toArbitrary: () => (fc) => fc.json() + }), + defectTransformation + )) +]) + +/** + * Schema for `Exit` values, representing either a success with value `A` or a + * failure with a `Cause` containing typed errors and defects. + * + * @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. + * + * @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 Issue.InvalidType(ast, Option_.some(input))) + } + switch (input._tag) { + case "Success": + return Effect.mapBothEager( + Parser.decodeUnknownEffect(value)(input.value, options), + { + onSuccess: Exit_.succeed, + onFailure: (issue) => + new Issue.Composite(ast, Option_.some(input), [new Issue.Pointer(["value"], issue)]) + } + ) + case "Failure": + return Effect.mapBothEager( + Parser.decodeUnknownEffect(cause)(input.cause, options), + { + onSuccess: Exit_.failCause, + onFailure: (issue) => + new Issue.Composite(ast, Option_.some(input), [new Issue.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) }) + ]), + Transformation.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) => + fc.oneof( + ctx?.isSuspend ? { maxDepth: 2, depthIdentifier: "Exit" } : {}, + value.map((v) => Exit_.succeed(v)), + causeToArbitrary(error, defect)(fc, ctx).map((cause) => Exit_.failCause(cause)) + ), + 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 of a `ReadonlyMap` schema whose keys and values are + * validated by the provided schemas. + * + * **When to use** + * + * Use as a type annotation for a `ReadonlyMap` schema when exposing or returning + * the schema while preserving its key schema, value schema, and ISO + * representation. + * + * @see {@link ReadonlyMap} for constructing this schema type from key and value schemas + * @see {@link ReadonlyMapIso} for the readonly tuple-array ISO representation used by this schema + * + * @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 + +/** + * 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( + Parser.decodeUnknownEffect(array)([...input], options), + { + onSuccess: (array: ReadonlyArray) => new globalThis.Map(array), + onFailure: (issue) => + new Issue.Composite(ast, Option_.some(input), [new Issue.Pointer(["entries"], issue)]) + } + ) + } + return Effect.fail(new Issue.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])), + Transformation.transform({ + decode: (e) => new globalThis.Map(e), + encode: (map) => [...map.entries()] + }) + ), + toArbitrary: ([key, value]) => (fc, ctx) => { + return fc.oneof( + ctx?.isSuspend ? { maxDepth: 2, depthIdentifier: "ReadonlyMap" } : {}, + fc.constant([]), + fc.array(fc.tuple(key, value), ctx?.constraints?.array) + ).map((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 }) +} + +/** + * Schema for an Effect `HashMap` where keys and values must conform to the + * provided schemas. + * + * @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 + +/** + * 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( + Parser.decodeUnknownEffect(entries)(HashMap_.toEntries(input), options), + { + onSuccess: HashMap_.fromIterable, + onFailure: (issue) => + new Issue.Composite(ast, Option_.some(input), [new Issue.Pointer(["entries"], issue)]) + } + ) + } + return Effect.fail(new Issue.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])), + Transformation.transform({ + decode: HashMap_.fromIterable, + encode: HashMap_.toEntries + }) + ), + toArbitrary: ([key, value]) => (fc, ctx) => { + return fc.oneof( + ctx?.isSuspend ? { maxDepth: 2, depthIdentifier: "HashMap" } : {}, + fc.constant([]), + fc.array(fc.tuple(key, value), ctx?.constraints?.array) + ).map(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 of a `ReadonlySet` schema whose values are validated + * by the provided element schema. + * + * **When to use** + * + * Use to name or constrain the schema type produced by `ReadonlySet` when + * generic code needs to retain the element schema type. + * + * @see {@link ReadonlySet} for constructing this schema type + * @see {@link ReadonlySetIso} for the array representation used by this schema's ISO + * + * @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( + Parser.decodeUnknownEffect(array)([...input], options), + { + onSuccess: (array: ReadonlyArray) => new globalThis.Set(array), + onFailure: (issue) => + new Issue.Composite(ast, Option_.some(input), [new Issue.Pointer(["values"], issue)]) + } + ) + } + return Effect.fail(new Issue.InvalidType(ast, Option_.some(input))) + } + }, + { + typeConstructor: { + _tag: "ReadonlySet" + }, + generation: { + runtime: `Schema.ReadonlySet(?)`, + Type: `globalThis.ReadonlySet` + }, + expected: "ReadonlySet", + toCodec: ([value]) => + link>()( + ArraySchema(value), + Transformation.transform({ + decode: (e) => new globalThis.Set(e), + encode: (set) => [...set.values()] + }) + ), + toArbitrary: ([value]) => (fc, ctx) => { + return fc.oneof( + ctx?.isSuspend ? { maxDepth: 2, depthIdentifier: "ReadonlySet" } : {}, + fc.constant([]), + fc.array(value, ctx?.constraints?.array) + ).map((as) => new globalThis.Set(as)) + }, + 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 }) +} + +/** + * Schema for an Effect `HashSet` where values must conform to the provided + * schema. + * + * @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( + Parser.decodeUnknownEffect(values)(Arr.fromIterable(input), options), + { + onSuccess: HashSet_.fromIterable, + onFailure: (issue) => + new Issue.Composite(ast, Option_.some(input), [new Issue.Pointer(["values"], issue)]) + } + ) + } + return Effect.fail(new Issue.InvalidType(ast, Option_.some(input))) + } + }, + { + typeConstructor: { + _tag: "effect/HashSet" + }, + generation: { + runtime: `Schema.HashSet(?)`, + Type: `HashSet.HashSet` + }, + expected: "HashSet", + toCodec: ([value]) => + link>()( + ArraySchema(value), + Transformation.transform({ + decode: HashSet_.fromIterable, + encode: Arr.fromIterable + }) + ), + toArbitrary: ([value]) => (fc, ctx) => { + return fc.oneof( + ctx?.isSuspend ? { maxDepth: 2, depthIdentifier: "HashSet" } : {}, + fc.constant([]), + fc.array(value, ctx?.constraints?.array) + ).map(HashSet_.fromIterable) + }, + 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 }) +} + +/** + * Schema for an Effect `Chunk` (immutable array-like collection) where values + * must conform to the provided schema. + * + * @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( + Parser.decodeUnknownEffect(values)(Arr.fromIterable(input), options), + { + onSuccess: Chunk_.fromIterable, + onFailure: (issue) => + new Issue.Composite(ast, Option_.some(input), [new Issue.Pointer(["values"], issue)]) + } + ) + } + return Effect.fail(new Issue.InvalidType(ast, Option_.some(input))) + } + }, + { + typeConstructor: { + _tag: "effect/Chunk" + }, + generation: { + runtime: `Schema.Chunk(?)`, + Type: `Chunk.Chunk` + }, + expected: "Chunk", + toCodec: ([value]) => + link>()( + ArraySchema(value), + Transformation.transform({ + decode: Chunk_.fromIterable, + encode: Arr.fromIterable + }) + ), + toArbitrary: ([value]) => (fc, ctx) => { + return fc.oneof( + ctx?.isSuspend ? { maxDepth: 2, depthIdentifier: "Chunk" } : {}, + fc.constant([]), + fc.array(value, ctx?.constraints?.array) + ).map(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 the schema for JavaScript `RegExp` instances. + * + * @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 + }), + Transformation.transformOrFail({ + decode: (e) => + Effect.try({ + try: () => new globalThis.RegExp(e.source, e.flags), + catch: (e) => new Issue.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 the schema for JavaScript `URL` instances. + * + * @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, + Transformation.urlFromString + ), + toArbitrary: () => (fc) => fc.webUrl().map((s) => new globalThis.URL(s)), + toEquivalence: () => (a, b) => a.toString() === b.toString() + } +) + +/** + * Type-level representation of a transformation schema that decodes valid URL + * strings into JavaScript `URL` instances. + * + * @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, Transformation.urlFromString)) + +/** + * Type-level representation of the schema for JavaScript `Date` instances, + * including invalid dates. + * + * @category Date + * @since 4.0.0 + */ +export interface Date extends instanceOf { + readonly "Rebuild": Date +} + +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` + * instances. + * + * **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** (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 + * + * @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, + Transformation.dateFromString + ), + toArbitrary: () => (fc, ctx) => fc.date(ctx?.constraints?.date) + } +) + +/** + * Type-level representation of a transformation schema that decodes strings into + * JavaScript `Date` instances. + * + * @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 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, Transformation.dateFromString)) + +/** + * Type-level representation of the `DateValid` schema, which accepts only valid + * JavaScript `Date` instances. + * + * @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 the schema for Effect `Duration` values. + * + * @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** (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 }) + ]), + Transformation.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 a transformation schema that decodes strings + * accepted by `Duration.fromInput` into `Duration` values. + * + * @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, Transformation.durationFromString) +) + +/** + * Type-level representation of a transformation schema that decodes non-negative + * nanosecond `bigint` values into `Duration` values. + * + * @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, Transformation.durationFromNanos) +) + +/** + * Type-level representation of a transformation schema that decodes + * non-negative millisecond numbers into `Duration` values. + * + * @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, Transformation.durationFromMillis) +) + +/** + * Type-level representation of the schema for Effect `BigDecimal` values. + * + * @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" }) + +/** + * Schema for `BigDecimal` values. + * + * **When to use** + * + * Use when values are already 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, + Transformation.bigDecimalFromString + ), + toArbitrary: () => (fc) => + fc.tuple(fc.bigInt(), fc.integer({ min: 0, max: 20 })) + .map(([value, scale]) => BigDecimal_.make(value, scale)), + toFormatter: () => (bd) => BigDecimal_.format(bd), + toEquivalence: () => BigDecimal_.Equivalence + } +) + +/** + * Type-level representation of a transformation schema that decodes strings into + * `BigDecimal` values. + * + * @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, Transformation.bigDecimalFromString) +) + +/** + * Type-level representation of a transformation schema that decodes + * JSON-encoded strings into `unknown` values. + * + * @category JSON + * @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 JSON + * @since 4.0.0 + */ +export const UnknownFromJsonString: UnknownFromJsonString = fromJsonString(Unknown) + +/** + * Type-level representation of a schema that parses a JSON string and then + * decodes the parsed value with the provided schema. + * + * @category JSON + * @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 JSON + * @since 4.0.0 + */ +export function fromJsonString(schema: S): fromJsonString { + return String.annotate({ + expected: "a string that will be decoded as JSON", + contentMediaType: "application/json", + contentSchema: AST.toEncoded(schema.ast) + }).pipe(decodeTo(schema, Transformation.fromJsonString)) +} + +/** + * Type-level representation of the schema for JavaScript `File` instances. + * + * @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 + }), + Transformation.transformOrFail({ + decode: (e) => + Result_.match(Encoding.decodeBase64(e.data), { + onFailure: (error) => + Effect.fail( + new Issue.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 Issue.InvalidValue(Option_.some(file), { + message: globalThis.String(e) + }) + }) + }) + ) +}) + +/** + * Type-level representation of the schema for JavaScript `FormData` instances. + * + * @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 }) + ]) + ]) + ), + Transformation.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 of a schema that parses `FormData` into a tree + * record and then decodes it with the provided schema. + * + * @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 values that are not strings, use + * `Schema.toCodecStringTree` with the `keepDeclarations: true` option. + * This serializer preserves values such as numbers and `Blob` objects when + * compatible with the 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"}) + * ``` + * + * **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"}}) + * ``` + * + * **Example** (Parsing non-string values) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.fromFormData( + * Schema.toCodecStringTree( + * Schema.Struct({ + * a: Schema.Int + * }), + * { keepDeclarations: true } + * ) + * ) + * + * 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, Transformation.fromFormData)) +} + +/** + * Type-level representation of the schema for JavaScript `URLSearchParams` + * instances. + * + * @category URLSearchParams + * @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 URLSearchParams + * @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" }), + Transformation.transform({ + decode: (e) => new globalThis.URLSearchParams(e), + encode: (params) => params.toString() + }) + ) +}) + +/** + * Type-level representation of a schema that parses `URLSearchParams` into a + * tree record and then decodes it with the provided schema. + * + * @category URLSearchParams + * @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** (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, Transformation.fromURLSearchParams)) +} + +/** + * Type-level representation of the `Finite` number schema, which rejects `NaN`, + * `Infinity`, and `-Infinity`. + * + * @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 the `Int` schema, which accepts only finite + * integer numbers. + * + * @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 a transformation schema that decodes strings into + * numbers using JavaScript number coercion. + * + * @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, Transformation.numberFromString)) + +/** + * Type-level representation of a transformation schema that decodes strings into + * finite numbers. + * + * @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, Transformation.numberFromString)) + +/** + * Type-level representation of a transformation schema that decodes strings into + * `bigint` values. + * + * @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(AST.bigIntString).pipe( + decodeTo(BigInt, Transformation.bigintFromString) +) + +/** + * Schema interface for `Trimmed`, representing strings with no leading or + * trailing whitespace. + * + * @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()) + +/** + * Schema interface for `Trim`, a transformation that trims leading and trailing + * whitespace while decoding and encodes the trimmed string unchanged. + * + * @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, Transformation.trim())) + +/** + * Schema interface for `StringFromBase64`, a transformation between RFC4648 + * base64-encoded strings and UTF-8 strings. + * + * @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, Transformation.stringFromBase64String) +) + +/** + * Schema interface for `StringFromBase64Url`, a transformation between URL-safe + * base64-encoded strings and UTF-8 strings. + * + * @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, Transformation.stringFromBase64UrlString) +) + +/** + * Schema interface for `StringFromHex`, a transformation between hex-encoded + * strings and UTF-8 strings. + * + * @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, Transformation.stringFromHexString) +) + +/** + * Schema interface for `StringFromUriComponent`, a transformation between + * URI-component encoded strings and UTF-8 strings. + * + * @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, Transformation.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 StandardSchema + * @since 4.0.0 + */ +export const StandardSchemaV1FailureResult = Struct({ + issues: ArraySchema(Struct({ + message: String, + path: optional(ArraySchema(Union([PropertyKey, Struct({ key: PropertyKey })]))) + })) +}) + +/** + * Schema interface for `BooleanFromBit`, a transformation between bit literals + * `0 | 1` and boolean values. + * + * @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, + Transformation.transform({ + decode: (bit) => bit === 1, + encode: (bool) => bool ? 1 : 0 + }) + ) +) + +/** + * Schema interface for `Uint8Array`, representing JavaScript `Uint8Array` + * instances with base64 JSON encoding. + * + * @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, + Transformation.uint8ArrayFromBase64String + ), + toArbitrary: () => (fc) => fc.uint8Array() +}) + +/** + * Schema interface for `Uint8ArrayFromBase64`, a transformation between + * base64-encoded strings and `Uint8Array` values. + * + * @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, Transformation.uint8ArrayFromBase64String) +) + +/** + * Schema interface for `Uint8ArrayFromBase64Url`, a transformation between + * URL-safe base64-encoded strings and `Uint8Array` values. + * + * @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: Getter.decodeBase64Url(), + encode: Getter.encodeBase64Url() + }) +) + +/** + * Schema interface for `Uint8ArrayFromHex`, a transformation between + * hex-encoded strings and `Uint8Array` values. + * + * @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: Getter.decodeHex(), + encode: Getter.encodeHex() + }) +) + +/** + * Schema interface for `DateTimeUtc`, representing `DateTime.Utc` values with + * UTC ISO string JSON encoding. + * + * @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, + Transformation.dateTimeUtcFromString + ), + toArbitrary: () => (fc, ctx) => + fc.date({ noInvalidDate: true, ...ctx?.constraints?.date }).map((date) => DateTime.fromDateUnsafe(date)), + toFormatter: () => (utc) => utc.toString(), + toEquivalence: () => DateTime.Equivalence + } +) + +/** + * Schema interface for `DateTimeUtcFromDate`, a transformation from valid + * JavaScript `Date` values to `DateTime.Utc`. + * + * @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 a boundary provides valid JavaScript `Date` objects but the decoded + * model should use `DateTime.Utc`. + * + * **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: Getter.dateTimeUtcFromInput(), + encode: Getter.transform(DateTime.toDateUtc) + }) +) + +/** + * Schema interface for `DateTimeUtcFromString`, a transformation from date-time + * strings to `DateTime.Utc`. + * + * @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. + * + * @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, + Transformation.dateTimeUtcFromString + ) +) + +/** + * Schema interface for `DateTimeUtcFromMillis`, a transformation between epoch + * milliseconds and `DateTime.Utc` values. + * + * @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. + * + * @category DateTime + * @since 4.0.0 + */ +export const DateTimeUtcFromMillis: DateTimeUtcFromMillis = Number.pipe( + decodeTo(DateTimeUtc, { + decode: Getter.dateTimeUtcFromInput(), + encode: Getter.transform(DateTime.toEpochMillis) + }) +) + +/** + * Schema interface for `TimeZoneOffset`, representing + * `DateTime.TimeZone.Offset` values encoded as offset milliseconds. + * + * @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, + Transformation.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 + } +) + +/** + * Schema interface for `TimeZoneNamed`, representing + * `DateTime.TimeZone.Named` values encoded as IANA time zone identifiers. + * + * @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, + Transformation.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 + } +) + +/** + * Schema interface for `TimeZoneNamedFromString`, a transformation between IANA + * time zone identifier strings and `DateTime.TimeZone.Named` values. + * + * @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, Transformation.timeZoneNamedFromString) +) + +/** + * Schema interface for `TimeZone`, representing `DateTime.TimeZone` values + * encoded as either IANA identifiers or numeric offset strings. + * + * @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, + Transformation.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) + } +) + +/** + * Schema interface for `TimeZoneFromString`, a transformation from IANA + * identifier or offset strings to `DateTime.TimeZone` values. + * + * @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, Transformation.timeZoneFromString) +) + +/** + * Schema interface for `DateTimeZoned`, representing `DateTime.Zoned` values + * with ISO offset or named-zone string JSON encoding. + * + * @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, + Transformation.dateTimeZonedFromString + ), + toArbitrary: () => (fc, ctx) => + fc.tuple( + fc.date({ + noInvalidDate: true, + min: new globalThis.Date(-8640000000000000 + 14 * 60 * 60 * 1000), + max: new globalThis.Date(8640000000000000 - 14 * 60 * 60 * 1000), + ...ctx?.constraints?.date + }), + 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 + } +) + +/** + * Schema interface for `DateTimeZonedFromString`, a transformation between + * zoned date-time strings and `DateTime.Zoned` values. + * + * @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, Transformation.dateTimeZonedFromString) +) + +// ----------------------------------------------------------------------------- +// Class +// ----------------------------------------------------------------------------- + +/** + * Interface for schema-backed classes created with {@link Class}. + * + * **Details** + * + * A `Class` is a TypeScript class whose constructor validates its input + * against a {@link Struct} schema. Instances are always structurally valid. + * + * The interface exposes the schema's `fields`, an `identifier` string, and + * helpers such as `mapFields`, `annotate`, `check`, and `extend`. + * + * @category models + * @since 3.10.0 + */ +export interface Class extends + Bottom< + Self, + S["Encoded"], + S["DecodingServices"], + S["EncodingServices"], + AST.Declaration, + decodeTo, S>, + RequiredKeys extends never ? void | S["~type.make.in"] : S["~type.make.in"], + S["Iso"], + readonly [S], + Self, + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + 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>> + + extend( + identifier: string + ): ( + fields: NewFields, + annotations?: Annotations.Declaration>>]> + ) => [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 + +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) { + input = input ?? {} + const validated = struct.make(input, options) + super({ ...input, ...validated }, { ...options, disableChecks: true }) + } + + static readonly [TypeId] = TypeId + + get [ClassTypeId]() { + return ClassTypeId + } + + static readonly [immerable] = true + + static readonly identifier = identifier + static readonly fields = struct.fields + + static get ast(): AST.Declaration { + return getClassSchema(this).ast + } + static pipe() { + return Pipeable.pipeArguments(this, arguments) + } + static rebuild(ast: AST.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 Parser.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(AST.annotate(this.ast, annotations)) + } + static annotateKey(annotations: Annotations.Key) { + return this.rebuild(AST.annotateKey(this.ast, annotations)) + } + static check(...checks: readonly [AST.Check, ...Array>]) { + return this.rebuild(AST.appendChecks(this.ast, checks)) + } + static extend( + identifier: string + ): ( + fields: NewFields, + annotations?: Annotations.Declaration>>]> + ) => Class>>, Self> { + return (newFields, annotations) => { + const fields = { ...struct.fields, ...newFields } + return makeClass( + this, + identifier, + makeStruct(AST.struct(fields, struct.ast.checks, { identifier }), 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 Transformation.Transformation( + Getter.transform((input) => new self(input)), + Getter.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) { + const transformation = getClassTransformation(self) + const to = make>( + new AST.Declaration( + [from.ast], + () => (input, ast) => { + return input instanceof self || + Predicate.hasProperty(input, getClassTypeId(identifier)) ? + Effect.succeed(input) : + Effect.fail(new Issue.InvalidType(ast, Option_.some(input))) + }, + { + identifier, + [AST.ClassTypeId]: ([from]: readonly [AST.AST]) => new AST.Link(from, transformation), + toCodec: ([from]: readonly [Codec]) => new AST.Link(from.ast, transformation), + toArbitrary: ([from]: readonly [FastCheck.Arbitrary]) => () => + from.map((args) => new self(args)), + toFormatter: ([from]: readonly [Formatter]) => (t: Self) => `${self.identifier}(${from(t)})`, + "~sentinels": AST.collectSentinels(from.ast), + ...annotations + } + ) + ) + memo = from.pipe(decodeTo(to, transformation)) + } + return memo + } +} + +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 to define a schema-backed data class when you want 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** (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** (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** (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 just need the arbitrary directly. + * + * @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. + * + * **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 { + 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: Schema, options?: { + readonly onBefore?: + | ((ast: AST.AST, recur: (ast: AST.AST) => Formatter) => Formatter | undefined) + | undefined +}): Formatter { + return recur(schema.ast) + + function recur(ast: AST.AST): Formatter { + // --------------------------------------------- + // handle annotation + // --------------------------------------------- + const annotation = InternalAnnotations.resolve(ast)?.["toFormatter"] + if (typeof annotation === "function") { + return annotation(AST.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: AST.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 (AST.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++) { + i += j + out.push(tail[j](t[i])) + } + } + + 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 (AST.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 = AST.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) => AST.getCandidates(t, ast.types) + return (t) => { + const candidates = getCandidates(t) + const refinements = candidates.map(Parser._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 = AST.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 the default structural equivalence derived by {@link toEquivalence} + * is not appropriate for a type. + * + * @category Equivalence + * @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** (Struct equivalence) + * + * ```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 Equivalence + * @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: Top): SchemaRepresentation.Document { + return InternalStandard.fromAST(schema.ast) +} + +// ----------------------------------------------------------------------------- +// JsonSchema +// ----------------------------------------------------------------------------- + +/** + * Options for {@link toJsonSchemaDocument}. + * + * @category JsonSchema + * @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. + * + * @category JsonSchema + * @since 4.0.0 + */ +export function toJsonSchemaDocument(schema: Top, 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 +// ----------------------------------------------------------------------------- + +/** + * 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: Codec): Codec { + return make(toCodecJsonTop(schema.ast)) +} + +const toCodecJsonTop = AST.toCodec((ast) => { + const out = toCodecJsonBase(ast, toCodecJsonTop) + return out !== ast && AST.isOptional(ast) ? AST.optionalKeyLastLink(out) : out +}) + +function toCodecJsonBase(ast: AST.AST, recur: (ast: AST.AST) => AST.AST): AST.AST { + switch (ast._tag) { + case "Declaration": { + const getLink = ast.annotations?.toCodecJson ?? ast.annotations?.toCodec + if (Predicate.isFunction(getLink)) { + const tps = AST.isDeclaration(ast) + ? ast.typeParameters.map((tp) => InternalSchema.make(AST.toEncoded(tp))) + : [] + const link = getLink(tps) + const to = recur(link.to) + return AST.replaceEncoding(ast, to === link.to ? [link] : [new AST.Link(to, link.transformation)]) + } + return AST.replaceEncoding(ast, [AST.unknownToNull]) + } + case "Unknown": + case "ObjectKeyword": + return AST.replaceEncoding(ast, [AST.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) + } + case "Union": { + const sortedTypes = InternalSchema.jsonReorder(ast.types) + if (sortedTypes !== ast.types) { + return new AST.Union( + sortedTypes, + ast.mode, + ast.annotations, + ast.checks, + ast.encoding, + ast.context + ).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(AST.toType(schema.ast))) +} + +const toCodecIsoTop = memoize((ast: AST.AST): AST.AST => { + const out = toCodecIsoBase(ast, toCodecIsoTop) + return out !== ast && AST.isOptional(ast) ? AST.optionalKeyLastLink(out) : out +}) + +function toCodecIsoBase(ast: AST.AST, recur: (ast: AST.AST) => AST.AST): AST.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 AST.replaceEncoding(ast, to === link.to ? [link] : [new AST.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 + +/** + * 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). + * + * Options: + * + * - `keepDeclarations`: if `true`, it **does not** convert declarations to + * `undefined` but instead keeps them as they are (unless they have a + * `toCodecJson` or `toCodec` annotation). + * + * Defaults to `false`. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export function toCodecStringTree(schema: Codec): Codec +export function toCodecStringTree( + schema: Codec, + options: { readonly keepDeclarations: true } // Used in FormData +): Codec +export function toCodecStringTree( + schema: Codec, + options?: { readonly keepDeclarations?: boolean | undefined } +): Codec { + return make( + toCodecEnsureArray( + options?.keepDeclarations === true + ? serializerStringTreeKeepDeclarations(schema.ast) + : serializerStringTree(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: Codec, + 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: AST.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: AST.AST, + recur: (ast: AST.AST) => AST.AST, + onMissingAnnotation: (ast: AST.AST) => AST.AST +): AST.AST { + switch (ast._tag) { + case "Declaration": { + const getLink = ast.annotations?.toCodecJson ?? ast.annotations?.toCodec + if (Predicate.isFunction(getLink)) { + const tps = AST.isDeclaration(ast) + ? ast.typeParameters.map((tp) => make(recur(AST.toEncoded(tp)))) + : [] + const link = getLink(tps) + const to = recur(link.to) + return AST.replaceEncoding(ast, to === link.to ? [link] : [new AST.Link(to, link.transformation)]) + } + return onMissingAnnotation(ast) + } + case "Null": + return AST.replaceEncoding(ast, [nullToString]) + case "Boolean": + return AST.replaceEncoding(ast, [booleanToString]) + case "Unknown": + case "ObjectKeyword": + return AST.replaceEncoding(ast, [AST.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) + } + case "Union": { + const sortedTypes = treeReorder(ast.types) + if (sortedTypes !== ast.types) { + return new AST.Union( + sortedTypes, + ast.mode, + ast.annotations, + ast.checks, + ast.encoding, + ast.context + ).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 AST.Link( + new AST.Literal("null"), + new Transformation.Transformation( + Getter.transform(() => null), + Getter.transform(() => "null") + ) +) + +const booleanToString = new AST.Link( + new AST.Union([new AST.Literal("true"), new AST.Literal("false")], "anyOf"), + new Transformation.Transformation( + Getter.transform((s) => s === "true"), + Getter.String() + ) +) + +const serializerStringTree = AST.toCodec((ast) => { + const out = serializerTree(ast, serializerStringTree, (ast) => AST.replaceEncoding(ast, [unknownToUndefined])) + if (out !== ast && AST.isOptional(ast)) { + return AST.optionalKeyLastLink(out) + } + return out +}) + +const unknownToUndefined = new AST.Link( + AST.undefined, + new Transformation.Transformation( + Getter.passthrough(), + Getter.transform(() => undefined) + ) +) + +const serializerStringTreeKeepDeclarations = AST.toCodec((ast) => { + const out = serializerTree(ast, serializerStringTreeKeepDeclarations, identity) + if (out !== ast && AST.isOptional(ast)) { + return AST.optionalKeyLastLink(out) + } + return out +}) + +const SERIALIZER_ENSURE_ARRAY = "~effect/Schema/SERIALIZER_ENSURE_ARRAY" + +const toCodecEnsureArray = AST.toCodec((ast) => { + if (AST.isUnion(ast) && ast.annotations?.[SERIALIZER_ENSURE_ARRAY]) { + return ast + } + const out = onSerializerEnsureArray(ast) + if (AST.isArrays(out)) { + const ensure = new AST.Union( + [ + out, + AST.decodeTo( + AST.string, + out, + new Transformation.Transformation( + Getter.split(), + Getter.passthrough() + ) + ) + ], + "anyOf", + { [SERIALIZER_ENSURE_ARRAY]: true } + ) + return AST.isOptional(ast) ? AST.optionalKey(ensure) : ensure + } + return out +}) + +function onSerializerEnsureArray(ast: AST.AST): AST.AST { + switch (ast._tag) { + default: + return ast + case "Declaration": + case "Arrays": + case "Objects": + case "Union": + case "Suspend": + return ast.recur(toCodecEnsureArray) + } +} + +// ----------------------------------------------------------------------------- +// 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(Parser.encodeSync(serializer), Parser.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() +} + +/** + * The schema type returned by {@link overrideToCodecIso}. Carries a custom + * `Iso` type parameter and exposes the original `schema`. + * + * @category Optic + * @since 4.0.0 + */ +export interface overrideToCodecIso extends + Bottom< + S["Type"], + S["Encoded"], + S["DecodingServices"], + S["EncodingServices"], + S["ast"], + overrideToCodecIso, + S["~type.make.in"], + Iso, + S["~type.parameters"], + S["~type.make"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + 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: Codec, + transformation: { + readonly decode: Getter.Getter + readonly encode: Getter.Getter + } +) { + return (schema: S): overrideToCodecIso => { + return make( + AST.annotate(schema.ast, { + toCodecIso: () => new AST.Link(to.ast, Transformation.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 JsonPatch + * @since 4.0.0 + */ +export function toDifferJsonPatch(schema: Codec): Differ { + const serializer = toCodecJson(schema) + const get = Parser.encodeSync(serializer) + const set = Parser.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 JSON + * @since 4.0.0 + */ +export type Json = null | number | boolean | string | JsonArray | JsonObject + +/** + * A readonly array of {@link Json} values. + * + * @category JSON + * @since 4.0.0 + */ +export interface JsonArray extends ReadonlyArray {} + +/** + * A readonly record whose values are {@link Json} values. + * + * @category JSON + * @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 JSON + * @since 4.0.0 + */ +export const Json: Codec = make(AST.Json) + +/** + * Recursive TypeScript type for mutable JSON values: `null`, `number`, + * `boolean`, `string`, mutable arrays, or mutable string-keyed records. + * + * @category JSON + * @since 4.0.0 + */ +export type MutableJson = null | number | boolean | string | MutableJsonArray | MutableJsonObject + +/** + * A mutable array of {@link MutableJson} values. + * + * @category JSON + * @since 4.0.0 + */ +export interface MutableJsonArray extends Array {} + +/** + * A mutable record whose values are {@link MutableJson} values. + * + * @category JSON + * @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 JSON + * @since 4.0.0 + */ +export const MutableJson: Codec = make(AST.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 3.10.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 3.10.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?: AST.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 3.10.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 3.10.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) => AST.Link) + | undefined + readonly toCodecJson?: + | ((typeParameters: TypeParameters.Encoded) => AST.Link) + | undefined + readonly toCodecIso?: + | ((typeParameters: TypeParameters.Type) => AST.Link) + | undefined + readonly toArbitrary?: ToArbitrary.Declaration | undefined + readonly toEquivalence?: ToEquivalence.Declaration | undefined + readonly toFormatter?: ToFormatter.Declaration | undefined + readonly typeConstructor?: { + readonly _tag: string + } | 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 AST. + * + * @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 3.10.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 + readonly toArbitraryConstraint?: + | ToArbitrary.Constraint + | 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 and carry merged fast-check constraints. + * + * @since 4.0.0 + */ + export namespace ToArbitrary { + /** + * fast-check string constraints plus optional regular-expression pattern strings + * used when deriving string arbitraries from schema checks. + * + * @category models + * @since 4.0.0 + */ + export interface StringConstraints extends FastCheck.StringSharedConstraints { + readonly patterns?: readonly [string, ...Array] + } + + /** + * fast-check floating-point constraints plus `isInteger`, which switches + * derived number arbitraries to integer generation. + * + * @category models + * @since 4.0.0 + */ + export interface NumberConstraints extends FastCheck.FloatConstraints { + readonly isInteger?: boolean + } + + /** + * fast-check bigint constraints used when deriving arbitraries for bigint + * schemas. + * + * @category models + * @since 4.0.0 + */ + export interface BigIntConstraints extends FastCheck.BigIntConstraints {} + + /** + * fast-check array constraints plus an optional comparator used when deriving + * unique-array arbitraries. + * + * @category models + * @since 4.0.0 + */ + export interface ArrayConstraints extends FastCheck.ArrayConstraints { + readonly comparator?: (a: any, b: any) => boolean + } + + /** + * fast-check date constraints used when deriving arbitraries for `Date` and + * DateTime schemas. + * + * @category models + * @since 4.0.0 + */ + export interface DateConstraints extends FastCheck.DateConstraints {} + + /** + * Grouped arbitrary-generation constraints accumulated from schema checks and + * passed to `toArbitrary` derivation. + * + * @category models + * @since 4.0.0 + */ + export interface Constraint { + readonly string?: StringConstraints | undefined + readonly number?: NumberConstraints | undefined + readonly bigint?: BigIntConstraints | undefined + readonly array?: ArrayConstraints | undefined + readonly date?: DateConstraints | undefined + } + + /** + * Context passed to arbitrary-derivation hooks, including accumulated + * constraints and an `isSuspend` flag used to limit recursion for suspended + * schemas. + * + * @category models + * @since 3.10.0 + */ + export interface Context { + /** + * This flag is set to `true` when the current schema is a suspend. The goal + * is to avoid infinite recursion when generating arbitrary values for + * suspends, so implementations should try to avoid excessive recursion. + */ + readonly isSuspend?: boolean | undefined + readonly constraints?: ToArbitrary.Constraint | undefined + } + + /** + * Hook signature for declaration schema arbitrary annotations. + * + * **Details** + * + * Given arbitraries for any type parameters, returns a function that receives the + * fast-check module and derivation context and produces an arbitrary for `T`. + * + * @category models + * @since 4.0.0 + */ + export interface Declaration> { + ( + /* Arbitraries for any type parameters of the schema (if present) */ + typeParameters: { readonly [K in keyof TypeParameters]: FastCheck.Arbitrary } + ): (fc: typeof FastCheck, context: Context) => FastCheck.Arbitrary + } + } + + /** + * 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 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: AST.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-smol/packages/effect/src/SchemaAST.ts b/.repos/effect-smol/packages/effect/src/SchemaAST.ts new file mode 100644 index 00000000000..d1f8e76d87f --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/SchemaAST.ts @@ -0,0 +1,3837 @@ +/** + * Abstract Syntax Tree (AST) representation for Effect schemas. + * + * This module defines the runtime data structures that represent schemas. + * Most users work with the `Schema` module directly; use `SchemaAST` when you + * need to inspect, traverse, or programmatically transform schema definitions. + * + * ## Mental model + * + * - **{@link AST}** — discriminated union (`_tag`) of all schema node types + * (e.g. `String`, `Objects`, `Union`, `Suspend`) + * - **{@link Base}** — abstract base class shared by every node; carries + * annotations, checks, encoding chain, and context + * - **{@link Encoding}** — a non-empty chain of {@link Link} values describing + * how to transform between the decoded (type) and encoded (wire) form + * - **{@link Check}** — a validation filter ({@link Filter} or + * {@link FilterGroup}) attached to an AST node + * - **{@link Context}** — per-property metadata: optionality, mutability, + * default values, key annotations + * - **Guards** — type-narrowing predicates for each AST variant (e.g. + * {@link isString}, {@link isObjects}) + * + * ## Common tasks + * + * - Inspect what kind of schema you have → guard functions ({@link isString}, + * {@link isObjects}, {@link isUnion}, etc.) + * - Get the decoded (type-level) AST → {@link toType} + * - Get the encoded (wire-format) AST → {@link toEncoded} + * - Swap decode/encode directions → {@link flip} + * - Read annotations → {@link resolve}, {@link resolveAt}, + * {@link resolveIdentifier} + * - Build a transformation between schemas → {@link decodeTo} + * - Add regex validation → {@link isPattern} + * + * ## Gotchas + * + * - AST nodes are structurally immutable; modification helpers return new + * objects via `Object.create`. + * - {@link Arrays} represents both tuples and arrays; {@link Objects} + * represents both structs and records. + * - {@link toType} and {@link toEncoded} are memoized — same input yields + * same output reference. + * - {@link Suspend} lazily resolves its inner AST via a thunk; the thunk is + * memoized on first call. + * + * ## Quickstart + * + * **Example** (Inspecting a schema's AST) + * + * ```ts + * import { Schema, SchemaAST } from "effect" + * + * const schema = Schema.Struct({ name: Schema.String, age: Schema.Number }) + * const ast = schema.ast + * + * if (SchemaAST.isObjects(ast)) { + * console.log(ast.propertySignatures.map(ps => ps.name)) + * // ["name", "age"] + * } + * + * const encoded = SchemaAST.toEncoded(ast) + * console.log(SchemaAST.isObjects(encoded)) // true + * ``` + * + * ## See also + * + * - {@link AST} + * - {@link toType} + * - {@link toEncoded} + * - {@link flip} + * - {@link resolve} + * + * @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 Option from "./Option.ts" +import * as Pipeable from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import * as RegEx from "./RegExp.ts" +import * as Result from "./Result.ts" +import type * as Schema from "./Schema.ts" +import * as Getter from "./SchemaGetter.ts" +import * as Issue from "./SchemaIssue.ts" +import type * as Parser from "./SchemaParser.ts" +import * as Transformation from "./SchemaTransformation.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 inspecting a schema AST and you need to 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 inspecting a schema AST and you need to 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 an 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: + | Transformation.Transformation + | Transformation.Middleware + + constructor( + to: AST, + transformation: + | Transformation.Transformation + | Transformation.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 models + * @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 none of the built-in AST nodes fit. The `run` function receives + * `typeParameters` and returns a parser that validates/transforms raw input. + * + * **Details** + * + * - `typeParameters` — inner schemas this declaration is parameterized over + * (e.g. the element type for a custom collection). + * - `run` — factory producing the actual parse function. + * + * @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 + + constructor( + typeParameters: ReadonlyArray, + run: ( + typeParameters: ReadonlyArray + ) => (input: unknown, self: Declaration, options: ParseOptions) => Effect.Effect, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context + ) { + super(annotations, checks, encoding, context) + this.typeParameters = typeParameters + this.run = run + } + /** @internal */ + getParser(): Parser.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) + } + } + /** @internal */ + recur(recur: (ast: AST) => AST) { + const tps = mapOrSame(this.typeParameters, recur) + return tps === this.typeParameters ? + this : + new Declaration(tps, this.run, this.annotations, this.checks, undefined, this.context) + } + /** @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 Transformation.Transformation( + Getter.transform(() => undefined), + Getter.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 the `void` type (accepts `undefined` at runtime). + * + * **Details** + * + * Behaves like {@link Undefined} for parsing but represents the TypeScript + * `void` type semantically. + * + * @see {@link void_ void} + * @see {@link isVoid} + * @category models + * @since 4.0.0 + */ +export class Void extends Base { + readonly _tag = "Void" + /** @internal */ + getParser() { + return fromConst(this, 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 that represent the TypeScript + * `void` type and accept `undefined` at runtime. + * + * @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 Transformation.Transformation( + Getter.transform((s) => coercions[s]), + Getter.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": + case "Literal": + case "TemplateLiteral": + return true + case "Union": + return 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 the + * template literal pattern. A regex is derived from the parts to validate + * 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) => Parser.Parser): Parser.Parser { + const parser = recur(this.asTemplateLiteralParser()) + return (oinput: Option.Option, options: ParseOptions) => + Effect.mapBothEager(parser(oinput, options), { + onSuccess: () => oinput, + onFailure: (issue) => new Issue.Composite(this, oinput, [issue]) + }) + } + /** @internal */ + getExpected(): string { + return "string" + } + /** @internal */ + asTemplateLiteralParser(): Arrays { + const tuple = new Arrays(false, this.parts.map(templateLiteralPartFromString), []) + const regExp = getTemplateLiteralRegExp(this) + return decodeTo( + string, + tuple, + new Transformation.Transformation( + Getter.transformOrFail((s: string) => { + const match = regExp.exec(s) + if (match) return Effect.succeed(match.slice(1, this.parts.length + 1)) + return Effect.fail( + new Issue.InvalidValue(Option.some(s), { + message: `Expected a value matching ${regExp.source}, got ${format(s)}` + }) + ) + }), + Getter.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 */ + 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 Transformation.Transformation( + Getter.transform(() => ast.literal), + Getter.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 */ + 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 */ + 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 defining or inspecting 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 */ + 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 */ + 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. + * + * **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 + + constructor( + isMutable: boolean, + elements: ReadonlyArray, + rest: ReadonlyArray, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context + ) { + super(annotations, checks, encoding, context) + this.isMutable = isMutable + this.elements = elements + this.rest = rest + + // 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) => Parser.Parser): Parser.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: Parser.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 Issue.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 Issue.Pointer([i], new Issue.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 Issue.Composite(ast, oinput, [issue])) + } + } + } + if (state.issues) { + return yield* Effect.fail(new Issue.Composite(ast, oinput, state.issues)) + } + return Option.some(state.output) + }) + } + /** @internal */ + recur(recur: (ast: AST) => AST) { + const elements = mapOrSame(this.elements, recur) + const rest = mapOrSame(this.rest, recur) + return elements === this.elements && rest === this.rest ? + this : + new Arrays(this.isMutable, elements, rest, this.annotations, this.checks, undefined, this.context) + } + /** @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: Parser.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 Issue.Pointer([i], new Issue.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 Issue.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 +) => { + const issueResult = Cause.findError(exit.cause) + if (Result.isFailure(issueResult)) { + return exit + } + const issue = new Issue.Pointer([key], issueResult.success) + if (s.options.errors === "all") { + if (s.issues) s.issues.push(issue) + else s.issues = [issue] + } else { + return Exit.fail(new Issue.Composite(ast, s.oinput, [issue])) + } +} + +/** + * floating point or integer, with optional exponent + * @internal + */ +export const FINITE_PATTERN = "[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?" + +const isNumberStringRegExp = new globalThis.RegExp(`(?:${FINITE_PATTERN}|Infinity|-Infinity|NaN)`) + +/** + * Returns the object keys that match the index signature parameter schema. + * @internal + */ +export function getIndexSignatureKeys( + input: { readonly [x: PropertyKey]: unknown }, + parameter: AST +): ReadonlyArray { + const encoded = toEncoded(parameter) + switch (encoded._tag) { + case "String": + return Object.keys(input) + case "TemplateLiteral": { + const regExp = getTemplateLiteralRegExp(encoded) + return Object.keys(input).filter((k) => regExp.test(k)) + } + case "Symbol": + return Object.getOwnPropertySymbols(input) + case "Number": + return Object.keys(input).filter((k) => isNumberStringRegExp.test(k)) + case "Union": + return [...new Set(encoded.types.flatMap((t) => getIndexSignatureKeys(input, t)))] + default: + return [] + } +} + +/** + * 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) + } +} + +/** + * Represents an index signature entry within an {@link Objects} node. + * + * **Details** + * + * - `parameter` — the key type AST (e.g. {@link String} for `string` keys, + * {@link TemplateLiteral} for patterned keys). + * - `type` — the value type AST. + * - `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: AST + readonly type: AST + readonly merge: KeyValueCombiner | undefined + + constructor( + parameter: AST, + type: AST, + merge: KeyValueCombiner | undefined + ) { + 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. + * + * **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 + + constructor( + propertySignatures: ReadonlyArray, + indexSignatures: ReadonlyArray, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context + ) { + super(annotations, checks, encoding, context) + this.propertySignatures = propertySignatures + this.indexSignatures = indexSignatures + + // 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) => Parser.Parser): Parser.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: Parser.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(indexSignatureParameterFromString(is.parameter)) + const effKey = parserKey(Option.some(key), s.options) + const exitKey = (effectIsExit(effKey) ? effKey : yield* Effect.exit(effKey)) as Exit.Exit< + Option.Option, + Issue.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 + 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 Issue.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 Issue.Pointer([key], new Issue.UnexpectedKey(ast, input[key])) + if (errorsAllOption) { + if (state.issues) { + state.issues.push(issue) + } else { + state.issues = [issue] + } + continue + } else { + return yield* Effect.fail(new Issue.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) + 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 Issue.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, + flipMerge: boolean + ): 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 = recur(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 + ? this + : new Objects(props, indexes, this.annotations, this.checks, undefined, this.context) + } + /** @internal */ + flip(recur: (ast: AST) => AST): AST { + return this.rebuild(recur, true) + } + /** @internal */ + recur(recur: (ast: AST) => AST): AST { + return this.rebuild(recur, false) + } + /** @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: Parser.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 Issue.Pointer([p.name], new Issue.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 Issue.Composite(s.ast, s.oinput, [issue]) + ) + } + } + } +}) + +function mergeChecks(checks: Checks | undefined, b: AST): Checks | undefined { + if (!checks) { + return b.checks + } + if (!b.checks) { + return checks + } + return [...checks, ...b.checks] +} + +/** @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 r of records) { + propertySignatures = propertySignatures.concat(r.propertySignatures) + indexSignatures = indexSignatures.concat(r.indexSignatures) + checks = mergeChecks(checks, r) + } + 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": + case "Void": + 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 (const a of types) { + const encoded = toEncoded(a) + if (isNever(encoded)) continue + + const types = getCandidateTypes(encoded) + const sentinels = collectSentinels(encoded) + + // by-type (always filled – cheap primary filter) + idx.byType ??= {} + for (const t of types) (idx.byType[t] ??= []).push(a) + + 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(a) + } + } else { // non-discriminated + idx.otherwise ??= {} + for (const t of types) (idx.otherwise[t] ??= []).push(a) + } + } + + 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") { + for (const [k, m] of idx.bySentinel) { + if (Object.hasOwn(input, k)) { + const match = m.get((input as any)[k]) + if (match) return [...match, ...base].filter(filterLiterals(input)) + } + } + } + return base + } + + // 2. Fallback: runtime-type dispatch only + return (idx.byType?.[runtimeType] ?? []).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" + + constructor( + types: ReadonlyArray, + mode: "anyOf" | "oneOf", + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context + ) { + super(annotations, checks, encoding, context) + this.types = types + this.mode = mode + } + /** @internal */ + getParser(recur: (ast: AST) => Parser.Parser): Parser.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) + if (!eff) { + return state.out ? Effect.succeed(state.out) : Effect.fail(new Issue.AnyOf(ast, input, state.issues ?? [])) + } + return Effect.flatMap(eff, (_) => { + return state.out ? Effect.succeed(state.out) : Effect.fail(new Issue.AnyOf(ast, input, state.issues ?? [])) + }) + } + } + /** @internal */ + recur(recur: (ast: AST) => AST) { + const types = mapOrSame(this.types, recur) + return types === this.types ? + this : + new Union(types, this.mode, this.annotations, this.checks, undefined, this.context) + } + /** @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) => Parser.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 issueResult = Cause.findError(exit.cause) + if (Result.isFailure(issueResult)) { + return exit + } + if (s.issues) s.issues.push(issueResult.success) + else s.issues = [issueResult.success] + } else { + if (s.out && s.ast.mode === "oneOf") { + s.successes.push(candidate) + return Exit.fail(new Issue.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 Transformation.Transformation( + Getter.Number(), + Getter.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** (Recursive schema AST) + * + * ```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 + ) { + super(annotations, checks, encoding, context) + this.thunk = memoizeThunk(thunk) + } + /** @internal */ + getParser(recur: (ast: AST) => Parser.Parser): Parser.Parser { + return recur(this.thunk()) + } + /** @internal */ + recur(recur: (ast: AST) => AST) { + return new Suspend(() => recur(this.thunk()), this.annotations, this.checks, 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) => Issue.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) => Issue.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) => Issue.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 Issue.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`. + * + * **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 + }, + toArbitraryConstraint: { + string: { + 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.checks === checks) { + return ast + } + return modifyOwnPropertyDescriptors(ast, (d) => { + d.checks.value = checks + }) +} + +/** @internal */ +export function appendChecks(ast: A, checks: Checks): A { + return replaceChecks(ast, ast.checks ? [...ast.checks, ...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 middlewareDecoding( + ast: AST, + middleware: Transformation.Middleware +): AST { + return appendTransformation(ast, middleware, toType(ast)) +} + +/** @internal */ +export function middlewareEncoding( + ast: AST, + middleware: Transformation.Middleware +): AST { + return appendTransformation(toEncoded(ast), middleware, ast) +} + +function appendTransformation( + from: AST, + transformation: + | Transformation.Transformation + | Transformation.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 Transformation.Transformation( + Getter.withDefault(defaultValue), + Getter.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: Transformation.Transformation +): A { + return appendTransformation(from, transformation, to) +} + +function parseParameter(ast: AST): { + literals: ReadonlyArray + parameters: ReadonlyArray +} { + switch (ast._tag) { + case "Literal": + return { + literals: Predicate.isPropertyKey(ast.literal) ? [ast.literal] : [], + parameters: [] + } + case "UniqueSymbol": + return { + literals: [ast.symbol], + parameters: [] + } + case "String": + case "Number": + case "Symbol": + case "TemplateLiteral": + return { + literals: [], + parameters: [ast] + } + case "Union": { + const out: { + literals: ReadonlyArray + parameters: ReadonlyArray + } = { literals: [], parameters: [] } + for (let i = 0; i < ast.types.length; i++) { + const parsed = parseParameter(ast.types[i]) + out.literals = out.literals.concat(parsed.literals) + out.parameters = out.parameters.concat(parsed.parameters) + } + return out + } + } + 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 + return out.recur?.(toType) ?? out +}) + +/** + * 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 AST. + * + * - 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 getTemplateLiteralSource(ast: TemplateLiteral, top: boolean): string { + return ast.encodedParts.map((part) => + handleTemplateLiteralASTPartParens(part, getTemplateLiteralASTPartPattern(part), top) + ).join("") +} + +/** @internal */ +export const getTemplateLiteralRegExp = memoize((ast: TemplateLiteral): RegExp => { + return new globalThis.RegExp(`^${getTemplateLiteralSource(ast, true)}$`) +}) + +function getTemplateLiteralASTPartPattern(part: TemplateLiteralPart): string { + switch (part._tag) { + case "Literal": + return RegEx.escape(globalThis.String(part.literal)) + case "String": + return STRING_PATTERN + case "Number": + return FINITE_PATTERN + case "BigInt": + return BIGINT_PATTERN + case "TemplateLiteral": + return getTemplateLiteralSource(part, false) + case "Union": + return part.types.map(getTemplateLiteralASTPartPattern).join("|") + } +} + +function handleTemplateLiteralASTPartParens(part: TemplateLiteralPart, s: string, top: boolean): string { + if (isUnion(part)) { + if (!top) { + return `(?:${s})` + } + } else if (!top) { + return s + } + return `(${s})` +} + +function fromConst( + ast: AST, + value: T +): Parser.Parser { + const succeed = Effect.succeedSome(value) + return (oinput) => { + if (oinput._tag === "None") { + return Effect.succeedNone + } + return oinput.value === value + ? succeed + : Effect.fail(new Issue.InvalidType(ast, oinput)) + } +} + +function fromRefinement( + ast: AST, + refinement: (input: unknown) => input is T +): Parser.Parser { + return (oinput) => { + if (oinput._tag === "None") { + return Effect.succeedNone + } + return refinement(oinput.value) + ? Effect.succeed(oinput) + : Effect.fail(new Issue.InvalidType(ast, oinput)) + } +} + +/** @internal */ +export const enumsToLiterals = memoize((ast: Enum): Union => { + return new Union( + ast.enums.map((e) => new Literal(e[1], { title: e[0] })), + "anyOf" + ) +}) + +/** @internal */ +export function toCodec(f: (ast: AST) => AST) { + function out(ast: AST): AST { + return ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, out)) : f(ast) + } + return memoize(out) +} + +const indexSignatureParameterFromString = toCodec((ast) => { + switch (ast._tag) { + default: + return ast + case "Number": + return ast.toCodecStringTree() + case "Union": + return ast.recur(indexSignatureParameterFromString) + } +}) + +const templateLiteralPartFromString = toCodec((ast) => { + switch (ast._tag) { + default: + return ast + case "String": + case "TemplateLiteral": + return ast + case "BigInt": + case "Number": + case "Literal": + return ast.toCodecStringTree() + case "Union": + return ast.recur(templateLiteralPartFromString) + } +}) + +/** + * any string, including newlines + * @internal + */ +export const STRING_PATTERN = "[\\s\\S]*?" + +const isStringFiniteRegExp = new globalThis.RegExp(`^${FINITE_PATTERN}$`) + +/** @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, + Transformation.numberFromString +) + +const numberToString = new Link( + new Union([finiteString, nonFiniteLiterals], "anyOf"), + Transformation.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, + Transformation.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 Transformation.Transformation( + Getter.transform((description) => globalThis.Symbol.for(isStringSymbolRegExp.exec(description)![1])), + Getter.transformOrFail((sym: symbol) => { + const key = globalThis.Symbol.keyFor(sym) + if (key !== undefined) { + return Effect.succeed(globalThis.String(sym)) + } + return Effect.fail( + new Issue.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 Issue.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 Issue.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 + } + onPath.add(u) + const ok = Array.isArray(u) + ? 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 Issue.InvalidType(ast, Option.some(input))), + { + typeConstructor: { + _tag: "effect/Json" + }, + generation: { + runtime: `Schema.Json`, + Type: `Schema.Json` + }, + expected: "JSON value", + toCodecJson: () => new Link(unknown, Transformation.passthrough()) + } +) + +/** @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 Transformation.Transformation( + Getter.passthrough(), + Getter.transform(() => null) + ) +) + +/** @internal */ +export const unknownToJson = new Link( + Json, + Transformation.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 Issue.InvalidType(ast, Option.some(input))), + { + expected: "StringTree", + toCodecStringTree: () => new Link(unknown, Transformation.passthrough()) + } +) + +/** @internal */ +export const unknownToStringTree = new Link( + StringTree, + Transformation.passthrough() +) diff --git a/.repos/effect-smol/packages/effect/src/SchemaGetter.ts b/.repos/effect-smol/packages/effect/src/SchemaGetter.ts new file mode 100644 index 00000000000..461af1b1842 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/SchemaGetter.ts @@ -0,0 +1,1923 @@ +/** + * Composable transformation primitives for the Effect Schema system. + * + * A `Getter` represents a single-direction transformation from an + * encoded type `E` to a decoded type `T`. Getters are the building blocks + * that `Schema.decodeTo` and `Schema.decode` use to define how values are + * transformed during encoding and decoding. They handle optionality + * (`Option` in, `Option` out), can fail with `Issue`, and can require + * Effect services via `R`. + * + * ## Mental model + * + * - **Getter**: A function `Option -> Effect, Issue, R>`. It + * transforms an optional encoded value into an optional decoded value, + * possibly failing or requiring services. + * - **Passthrough**: The identity getter — returns the input unchanged. Used + * when no transformation is needed. Optimized away during composition. + * - **Option-awareness**: Getters receive and return `Option` to handle + * missing keys in structs. `Option.None` means the key is absent. + * - **Composition**: Getters compose left-to-right via `.compose()`. A + * passthrough on either side is a no-op (identity optimization). + * - **Issue**: The error type for all getter failures (see `SchemaIssue`). + * + * ## Common tasks + * + * - Pass a value through unchanged → {@link passthrough} + * - Transform a value purely → {@link transform} + * - Transform a value with possible failure → {@link transformOrFail} + * - Transform with full Option control → {@link transformOptional} + * - Handle missing keys → {@link onNone}, {@link required}, {@link withDefault} + * - Handle present values → {@link onSome} + * - Validate a value with an effectful check → {@link checkEffect} + * - Produce a constant value → {@link succeed} + * - Always fail → {@link fail}, {@link forbidden} + * - Omit a value from output → {@link omit} + * - Coerce to a primitive type → {@link String}, {@link Number}, {@link Boolean}, {@link BigInt}, {@link Date} + * - Transform strings → {@link trim}, {@link capitalize}, {@link toLowerCase}, {@link toUpperCase}, {@link split}, {@link splitKeyValue}, {@link joinKeyValue} + * - Parse/stringify JSON → {@link parseJson}, {@link stringifyJson} + * - Encode/decode Base64 → {@link encodeBase64}, {@link decodeBase64}, {@link decodeBase64String} + * - Encode/decode Hex → {@link encodeHex}, {@link decodeHex}, {@link decodeHexString} + * - Encode/decode URI components → {@link encodeUriComponent}, {@link decodeUriComponent} + * - Parse DateTime → {@link dateTimeUtcFromInput} + * - Decode/encode FormData → {@link decodeFormData}, {@link encodeFormData} + * - Decode/encode URLSearchParams → {@link decodeURLSearchParams}, {@link encodeURLSearchParams} + * - Build nested tree from bracket paths → {@link makeTreeRecord} + * - Flatten nested tree to bracket paths → {@link collectBracketPathEntries} + * + * ## Gotchas + * + * - Getters are not bidirectional. To define a full encode/decode pair, supply + * both a `decode` and an `encode` getter to `Schema.decodeTo`. + * - `passthrough` requires `T === E` by default. Use `{ strict: false }` to + * bypass the type constraint, or use {@link passthroughSupertype} / {@link passthroughSubtype}. + * - `transform` skips `None` inputs (missing keys) — the function is only + * called when a value is present. Use `transformOptional` if you need to + * handle missing values. + * - `parseJson` without a `reviver` returns `Schema.MutableJson`. With a + * reviver, the return type widens to `unknown`. + * - `split` treats an empty string as an empty array, not `[""]`. + * + * ## Quickstart + * + * **Example** (Using SchemaGetter with Schema.decodeTo) + * + * ```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)("42") + * // result: 42 + * ``` + * + * ## See also + * + * - {@link Getter} — the core class + * - {@link transform} — most common constructor + * - {@link passthrough} — identity getter + * - {@link transformOrFail} — fallible transformation + * + * @since 4.0.0 + */ +import * as DateTime from "./DateTime.ts" +import * as Effect from "./Effect.ts" +import * as Encoding from "./Encoding.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 AST from "./SchemaAST.ts" +import * as Issue 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 to build custom schema transformations with `Schema.decodeTo` or `Schema.decode`. + * - Composing multiple transformation steps into a single getter. + * + * **Details** + * + * - A getter wraps a function `Option -> Effect, Issue, R>`. + * - Receives `Option.None` when the encoded key is absent (e.g. missing struct field). + * - Returns `Option.None` to omit the value from the decoded output. + * - Fails with `Issue` on invalid input. + * - May require Effect services via `R`. + * - `.map(f)` applies `f` to the decoded value (inside the `Some`), leaving `None` unchanged. + * - `.compose(other)` chains two getters: the output of `this` feeds 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: AST.ParseOptions + ) => Effect.Effect, Issue.Issue, R> + + constructor( + run: ( + input: Option.Option, + options: AST.ParseOptions + ) => Effect.Effect, Issue.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 a schema field should always decode to a fixed value. + * - You need a placeholder getter that produces a known default. + * + * **Details** + * + * - Pure, no side effects. + * - Always returns `Option.some(t)` regardless of whether input is `Some` or `None`. + * + * **Example** (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 a transformation should unconditionally reject 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** (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) => Issue.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 a field or direction (encode/decode) should be disallowed entirely. + * - You want a clear "forbidden" error message in schema validation output. + * + * **Details** + * + * - Always fails with `Issue.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 Issue.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 no transformation is needed between encoded and decoded types. + * - One side of a `decodeTo` pair (encode or decode) should be a no-op. + * + * **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** (Identity transformation) + * + * ```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 no runtime conversion is needed but the getter should be typed + * as producing a decoded/output type that is narrower than the encoded/input + * type. + * + * **Details** + * + * - Same singleton as {@link passthrough} — no allocation, optimized in composition. + * + * **Example** (Supertype passthrough) + * + * ```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 the encoded type is narrower than the decoded type. + * - You need type-safe passthrough without `{ strict: false }`. + * + * **Details** + * + * - Same singleton as {@link passthrough} — no allocation, optimized in composition. + * + * **Example** (Subtype passthrough) + * + * ```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 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** (Default timestamp for 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: AST.ParseOptions) => Effect.Effect, Issue.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 a struct field must be present in the encoded input. + * - You want schema validation to report a missing key error. + * + * **Details** + * + * - When input is `None`, fails with `Issue.MissingKey`. + * - When input is `Some`, passes it through unchanged. + * - Optional `annotations` customize the error message for the missing key. + * + * **Example** (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 Issue.MissingKey(annotations))) +} + +/** + * Creates a getter that handles present values (`Option.Some`), passing `None` through. + * + * **When to use** + * + * Use when you need to transform or validate only when a 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** (Transform 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: AST.ParseOptions) => Effect.Effect, Issue.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 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 Issue.Issue}). + * - Does not transform the value — input and output types are the same. + * + * **Example** (Effectful validation) + * + * ```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: AST.ParseOptions) => Effect.Effect< + undefined | boolean | Schema.FilterIssue, + never, + R + > +): Getter { + return onSome((t, options) => { + return f(t, options).pipe(Effect.flatMapEager((out) => { + const issue = Issue.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 have 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** (String to number transformation pair) + * + * ```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 the transformation may fail (e.g. parsing, validation). + * - The transformation needs Effect services or is async. + * + * **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: AST.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 to handle both `Some` and `None` cases. + * - You want to turn a present value into absent, or vice versa. + * + * **Details** + * + * - Pure, never fails. + * - Receives the full `Option` and must return `Option`. + * + * **Example** (Filter 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 a field should be excluded during decoding or encoding. + * + * **Details** + * + * - Always returns `Option.None` regardless of input. + * - Never fails. + * + * **Example** (Omit 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 a field may be `undefined` in the encoded input and should have a fallback. + * + * **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** (Default value for 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 string representation of an arbitrary encoded value. + * + * **Details** + * + * - Pure, never fails. + * - Delegates to `globalThis.String`. + * + * **Example** (Coerce to 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 numeric coercion of an encoded value. + * + * **Details** + * + * - Pure, never fails (may produce `NaN` for non-numeric inputs). + * - Delegates to `globalThis.Number`. + * + * **Example** (Coerce to 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 boolean coercion (truthiness check) of an encoded value. + * + * **Details** + * + * - Pure, never fails. + * - Delegates to `globalThis.Boolean`. + * + * **Example** (Coerce to 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 to convert strings, numbers, or booleans to `bigint`. + * + * **Details** + * + * - Delegates to `globalThis.BigInt`. + * - Throws at runtime if the input cannot be converted (e.g. non-numeric string). + * + * **Example** (Coerce to 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 to parse a string, number, or Date into a `Date` object. + * + * **Details** + * + * - Delegates to `new globalThis.Date(input)`. + * - Does not validate the result — may produce an invalid Date. + * + * **Example** (Coerce to 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** (Trim 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** (Capitalize 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** (Uncapitalize 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** (Snake to camel) + * + * ```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** (Camel to snake) + * + * ```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** (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** (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 an encoded value is a JSON string that needs to be parsed 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 `Issue.InvalidValue` containing the error message. + * + * **Example** (Parse JSON) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const parse = SchemaGetter.parseJson() + * // Getter + * ``` + * + * @see {@link stringifyJson} for the inverse operation + * + * @category Json + * @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 Issue.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 a decoded value needs to be serialized to JSON text during encoding. + * + * **Details** + * + * - Skips `None` inputs. + * - On thrown stringify failures, such as circular references, fails with + * `Issue.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** (Stringify JSON) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const stringify = SchemaGetter.stringifyJson() + * // Getter + * ``` + * + * @see {@link parseJson} for the inverse operation + * + * @category Json + * @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 Issue.InvalidValue(Option.some(input), { message: globalThis.String(e) }) + }) + ) +} + +/** + * Parses a string into a record of key-value pairs. + * + * **When to use** + * + * Use when an encoded string contains delimited key-value pairs (e.g. `"a=1,b=2"`). + * + * **Details** + * + * - Splits the string by `separator` (default `,`), then each pair by `keyValueSeparator` (default `=`). + * - Pairs missing a key or value are silently skipped. + * - Pure, never fails. + * + * **Example** (Parse 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 a decoded record needs to be serialized as a delimited key-value string. + * + * **Details** + * + * - Joins entries with `separator` (default `,`) and key/value with `keyValueSeparator` (default `=`). + * - Pure, never fails. + * + * **Example** (Join key-value record) + * + * ```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 an encoded string is a delimited list (e.g. CSV values). + * + * **Details** + * + * - Splits by `separator` (default `,`). + * - An empty string produces an empty array (not `[""]`). + * - Pure, never fails. + * + * **Example** (Split 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** + * + * - Pure, never fails. + * + * **Example** (Encode 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 + * @since 4.0.0 + */ +export function encodeBase64(): Getter { + return transform(Encoding.encodeBase64) +} + +/** + * Encodes a `Uint8Array` or string to a URL-safe Base64 string. + * + * **Details** + * + * - Pure, never fails. + * + * **Example** (Encode 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 + * @since 4.0.0 + */ +export function encodeBase64Url(): Getter { + return transform(Encoding.encodeBase64Url) +} + +/** + * Encodes a `Uint8Array` or string to a hexadecimal string. + * + * **Details** + * + * - Pure, never fails. + * + * **Example** (Encode 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 + * @since 4.0.0 + */ +export function encodeHex(): Getter { + return transform(Encoding.encodeHex) +} + +/** + * Decodes a Base64 string to a `Uint8Array`. + * + * **Details** + * + * - Fails with `Issue.InvalidValue` if the input is not valid Base64. + * + * **Example** (Decode 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 + * @since 4.0.0 + */ +export function decodeBase64(): Getter { + return transformOrFail((input) => + Effect.mapErrorEager( + Effect.fromResult(Encoding.decodeBase64(input)), + (e) => new Issue.InvalidValue(Option.some(input), { message: e.message }) + ) + ) +} + +/** + * Decodes a Base64 string to a UTF-8 `string`. + * + * **Details** + * + * - Fails with `Issue.InvalidValue` if the input is not valid Base64. + * + * **Example** (Decode 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 + * @since 4.0.0 + */ +export function decodeBase64String(): Getter { + return transformOrFail((input) => + Result.match(Encoding.decodeBase64String(input), { + onFailure: (e) => Effect.fail(new Issue.InvalidValue(Option.some(input), { message: e.message })), + onSuccess: Effect.succeed + }) + ) +} + +/** + * Decodes a URL-safe Base64 string to a `Uint8Array`. + * + * **Details** + * + * - Fails with `Issue.InvalidValue` if the input is not valid Base64Url. + * + * **Example** (Decode 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 + * @since 4.0.0 + */ +export function decodeBase64Url(): Getter { + return transformOrFail((input) => + Result.match(Encoding.decodeBase64Url(input), { + onFailure: (e) => Effect.fail(new Issue.InvalidValue(Option.some(input), { message: e.message })), + onSuccess: Effect.succeed + }) + ) +} + +/** + * Decodes a URL-safe Base64 string to a UTF-8 `string`. + * + * **Details** + * + * - Fails with `Issue.InvalidValue` if the input is not valid Base64Url. + * + * **Example** (Decode 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 + * @since 4.0.0 + */ +export function decodeBase64UrlString(): Getter { + return transformOrFail((input) => + Result.match(Encoding.decodeBase64UrlString(input), { + onFailure: (e) => Effect.fail(new Issue.InvalidValue(Option.some(input), { message: e.message })), + onSuccess: Effect.succeed + }) + ) +} + +/** + * Decodes a hexadecimal string to a `Uint8Array`. + * + * **Details** + * + * - Fails with `Issue.InvalidValue` if the input is not valid hex. + * + * **Example** (Decode 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 + * @since 4.0.0 + */ +export function decodeHex(): Getter { + return transformOrFail((input) => + Result.match(Encoding.decodeHex(input), { + onFailure: (e) => Effect.fail(new Issue.InvalidValue(Option.some(input), { message: e.message })), + onSuccess: Effect.succeed + }) + ) +} + +/** + * Decodes a hexadecimal string to a UTF-8 `string`. + * + * **Details** + * + * - Fails with `Issue.InvalidValue` if the input is not valid hex. + * + * **Example** (Decode 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 + * @since 4.0.0 + */ +export function decodeHexString(): Getter { + return transformOrFail((input) => + Result.match(Encoding.decodeHexString(input), { + onFailure: (e) => Effect.fail(new Issue.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** (Encode 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 `Issue.InvalidValue` if the input contains malformed percent-encoding sequences. + * + * **Example** (Decode 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 Issue.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 an encoded value represents a date/time and should be decoded 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 `Issue.InvalidValue` if the input cannot be parsed as a valid + * `DateTime`. + * + * **Example** (Parse 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 Issue.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 to parse `FormData` from HTTP requests into structured objects. + * + * **Details** + * + * - Pure, never fails. + * - Interprets bracket-path keys (e.g. `user[name]`, `items[0]`) to build nested objects/arrays. + * - Leaf values are `string` or `Blob`. + * + * **Example** (Decode 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 to serialize structured data to `FormData` for HTTP requests. + * + * **Details** + * + * - Pure, never fails. + * - Flattens nested objects/arrays into bracket-path keys (e.g. `user[name]`, `items[0]`). + * - Non-object inputs produce an empty `FormData`. + * + * **Example** (Encode 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 to parse query parameters from URLs into structured objects. + * + * **Details** + * + * - Pure, never fails. + * - Interprets bracket-path keys (e.g. `user[name]`, `items[0]`) to build nested objects/arrays. + * - Leaf values are `string`. + * + * **Example** (Decode 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 URLSearchParams + * @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 to serialize structured data to query parameters for URLs. + * + * **Details** + * + * - Pure, never fails. + * - Flattens nested objects/arrays into bracket-path keys. + * - Non-object inputs produce an empty `URLSearchParams`. + * + * **Example** (Encode 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 URLSearchParams + * @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)) +} + +/** + * Builds a nested tree object from a list of bracket-path entries. + * + * **When to use** + * + * Use 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** (Build 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 + + if (cur[index] === undefined) { + cur[index] = shouldBeArray ? [] : {} + } + + cur = cur[index] + } + } 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 + if (Array.isArray(cur[token])) { + cur[token].push(value) + } else if (Object.prototype.hasOwnProperty.call(cur, token)) { + cur[token] = [cur[token], value] + } else { + 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 === "" + + if (cur[token] === undefined) { + cur[token] = shouldBeArray ? [] : {} + } + + cur = cur[token] + } + }) + }) + return out +} + +/** + * Flattens a nested object into bracket-path entries, filtering leaf values by a type guard. + * + * **When to use** + * + * Use 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** (Flatten 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-smol/packages/effect/src/SchemaIssue.ts b/.repos/effect-smol/packages/effect/src/SchemaIssue.ts new file mode 100644 index 00000000000..483f19facbf --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/SchemaIssue.ts @@ -0,0 +1,1246 @@ +/** + * Structured validation errors produced by the Effect Schema system. + * + * When `Schema.decode`, `Schema.encode`, or a filter rejects a value, the + * result is an {@link Issue} — a recursive tree that describes *what* went + * wrong and *where*. This module defines every node type in that tree and + * provides formatters that turn an `Issue` into a human-readable string or a + * Standard Schema V1 failure result. + * + * ## Mental model + * + * - **Issue**: A discriminated union (`_tag`) of all possible validation error + * nodes. It is recursive — composite nodes wrap inner `Issue` children. + * - **Leaf**: A terminal issue with no inner children ({@link InvalidType}, + * {@link InvalidValue}, {@link MissingKey}, {@link UnexpectedKey}, + * {@link Forbidden}, {@link OneOf}). + * - **Composite nodes**: Wrap one or more inner issues to add context — + * {@link Filter}, {@link Encoding}, {@link Pointer}, {@link Composite}, + * {@link AnyOf}. + * - **Pointer**: Adds a property-key path to an inner issue, indicating + * *where* in the input the error occurred. + * - **Formatter**: A function `Issue → Format` that serialises the error tree. + * Two built-in factories are provided: {@link makeFormatterDefault} (plain + * string) and {@link makeFormatterStandardSchemaV1} (Standard Schema V1). + * + * ## Common tasks + * + * - Check if a value is an Issue → {@link isIssue} + * - Extract the actual input from any issue → {@link getActual} + * - Format an issue as a string → {@link makeFormatterDefault} + * - Format an issue for Standard Schema V1 → {@link makeFormatterStandardSchemaV1} + * - Customise leaf formatting → {@link defaultLeafHook} + * - Customise filter formatting → {@link defaultCheckHook} + * + * ## Gotchas + * + * - `Pointer` and `MissingKey` carry no actual value — {@link getActual} + * returns `Option.none()` for them. + * - `AnyOf`, `UnexpectedKey`, `OneOf`, and `Filter` store `actual` as a plain + * `unknown` (not `Option`), so {@link getActual} wraps them with + * `Option.some`. + * - Calling `toString()` on any `Issue` uses the default formatter. To + * customise output, create your own formatter with + * {@link makeFormatterDefault} or {@link makeFormatterStandardSchemaV1}. + * - The `Issue` tree can be deeply nested for complex schemas. Formatters + * flatten composite nodes for display. + * + * ## Quickstart + * + * **Example** (Inspecting a validation error) + * + * ```ts + * import { Schema, SchemaIssue } from "effect" + * + * const Person = Schema.Struct({ + * name: Schema.String, + * age: Schema.Number + * }) + * + * try { + * Schema.decodeUnknownSync(Person)({ name: 42 }) + * } catch (e) { + * if (Schema.isSchemaError(e)) { + * console.log(SchemaIssue.isIssue(e.issue)) + * // true + * console.log(String(e.issue)) + * // formatted error message + * } + * } + * ``` + * + * ## See also + * + * - {@link Issue} — the root union type + * - {@link Leaf} — terminal issue types + * - {@link Formatter} — the formatter interface + * - {@link makeFormatterDefault} — default string formatter + * + * @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 AST from "./SchemaAST.ts" + +const TypeId = "~effect/SchemaIssue/Issue" + +/** + * Returns `true` if the given value is an {@link Issue}. + * + * **When to use** + * + * Use when narrowing an `unknown` value to `Issue` in error-handling code. + * - 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. + * - Pattern-matching on the `_tag` of an issue when you only care about leaves. + * + * **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. + * - 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 which filter 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: AST.Filter + /** + * The issue that occurred. + */ + readonly issue: Issue + + constructor( + /** + * The input value that caused the issue. + */ + actual: unknown, + /** + * The filter that failed. + */ + filter: AST.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: AST.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: AST.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: AST.AST + /** + * The input value that caused the issue. + */ + readonly actual: unknown + + constructor( + /** + * The schema that caused the issue. + */ + ast: AST.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: AST.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: AST.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** (Formatted 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: AST.AST + /** + * The input value that caused the issue. + */ + readonly actual: Option.Option + + constructor( + /** + * The schema that caused the issue. + */ + ast: AST.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** (Custom filter returning InvalidValue) + * + * ```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: AST.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: AST.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: AST.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: AST.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 retrieve the offending value for logging or custom error rendering. + * - Uniformly access `actual` regardless of which issue variant you have. + * + * **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: AST.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 you only need to customise the + * {@link CheckHook}. + * - Reference as a starting point for custom `LeafHook` implementations. + * + * **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** (Using defaultLeafHook with Standard Schema formatter) + * + * ```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 you only need to customise 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 integrate with libraries that consume the + * [Standard Schema V1](https://github.com/standard-schema/standard-schema) + * error format. + * - Customise error rendering by providing `leafHook` and/or `checkHook`. + * + * **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: AST.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 produce error messages for logging, CLI output, or developer-facing + * diagnostics. + * - This is the default formatter used by `Issue.toString()`. + * + * **Details** + * + * - 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-smol/packages/effect/src/SchemaParser.ts b/.repos/effect-smol/packages/effect/src/SchemaParser.ts new file mode 100644 index 00000000000..b7aa324cbad --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/SchemaParser.ts @@ -0,0 +1,1038 @@ +/** + * Build reusable runtime parsers from Effect schemas. + * + * `SchemaParser` is the execution layer behind schema ASTs. It walks schema + * structure, applies transformations, merges parse options, runs checks, and + * reports failures as `SchemaIssue.Issue` values while exposing adapters for + * the result shape a boundary needs. + * + * **Mental model** + * + * - A schema has a decoded `Type` side and an `Encoded` side. + * - Decoders read `Encoded` or `unknown` input and produce `Type`. + * - Encoders read `Type` or `unknown` input and produce `Encoded`. + * - Maker helpers construct decoded `Type` values and apply constructor + * defaults before validation. + * - The same underlying parser can be adapted to `Effect`, `Promise`, `Exit`, + * `Result`, `Option`, a throwing synchronous function, or a type guard. + * + * **Common tasks** + * + * - Construct decoded values: {@link make}, {@link makeEffect}, + * {@link makeOption} + * - Decode untrusted boundary input: {@link decodeUnknownEffect}, + * {@link decodeUnknownSync}, {@link decodeUnknownResult} + * - Decode already typed encoded input: {@link decodeEffect}, + * {@link decodeSync} + * - Encode values back to their encoded representation: + * {@link encodeEffect}, {@link encodeSync}, {@link encodeUnknownEffect} + * - Check values without collecting issue details: {@link is}, {@link asserts} + * - Build directly from an AST: {@link run} + * + * **Gotchas** + * + * - `decodeUnknown*` accepts untyped input; `decode*` variants expect the + * schema's `Encoded` type. + * - `encodeUnknown*` accepts untyped input; `encode*` variants expect the + * schema's decoded `Type`. + * - Synchronous adapters cannot run asynchronous parsing work. Use `Effect` + * adapters when transformations require services or asynchronous effects. + * - Parse options supplied when a parser is created are merged with options + * supplied at call time, and schema-level parse annotations can further + * refine behavior. + * + * @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 { identity, memoize } from "./Function.ts" +import * as InternalAnnotations from "./internal/schema/annotations.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 AST from "./SchemaAST.ts" +import * as Issue from "./SchemaIssue.ts" + +const recurDefaults = memoize((ast: AST.AST): AST.AST => { + switch (ast._tag) { + case "Declaration": { + const getLink = ast.annotations?.[AST.ClassTypeId] + if (Predicate.isFunction(getLink)) { + const link = getLink(ast.typeParameters) + const to = recurDefaults(link.to) + return AST.replaceEncoding(ast, to === link.to ? [link] : [new AST.Link(to, link.transformation)]) + } + return ast + } + case "Objects": + case "Arrays": + return ast.recur((ast) => { + const defaultValue = ast.context?.defaultValue + if (defaultValue) { + return AST.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 issues 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 Constructing + * @since 4.0.0 + */ +export function makeEffect(schema: S) { + const ast = recurDefaults(AST.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. + * + * **When to use** + * + * Use when you only need to know whether constructor input is valid and do + * not need error details. + * + * @category Constructing + * @since 4.0.0 + */ +export function makeOption(schema: S) { + const parser = makeEffect(schema) + return (input: S["~type.make.in"], options?: Schema.MakeOptions): Option.Option => { + return Exit.getSuccess(Effect.runSyncExit(parser(input, options) as any)) + } +} + +/** + * 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. + * + * **Details** + * + * The returned function constructs a value from constructor input and throws an + * `Error` with the `SchemaIssue.Issue` in its `cause` when construction fails. + * + * @category Constructing + * @since 4.0.0 + */ +export function make(schema: S) { + const parser = makeEffect(schema) + return (input: S["~type.make.in"], options?: Schema.MakeOptions): S["Type"] => { + return Effect.runSync( + Effect.mapErrorEager( + parser(input, options), + (issue) => 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` on failure, without + * exposing issue details. + * + * @category Asserting + * @since 3.10.0 + */ +export function is(schema: Schema.Schema): (input: I) => input is I & T { + return _is(schema.ast) +} + +/** @internal */ +export function _is(ast: AST.AST) { + const parser = asExit(run(AST.toType(ast))) + return (input: I): input is I & T => { + return Exit.isSuccess(parser(input, AST.defaultParseOptions)) + } +} + +/** @internal */ +export function _issue(ast: AST.AST) { + const parser = run(ast) + return (input: unknown, options: AST.ParseOptions): Issue.Issue | undefined => { + return Effect.runSync(Effect.matchEager(parser(input, options), { + onSuccess: () => undefined, + onFailure: identity + })) + } +} + +/** + * 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, throwing + * when validation fails. + * + * **Details** + * + * The assertion returns normally when validation succeeds and throws when the + * input does not satisfy the schema. + * + * @category Asserting + * @since 4.0.0 + */ +export function asserts(schema: S, input: I): asserts input is I & S["Type"] { + const parser = asExit(run(AST.toType(schema.ast))) + const exit = parser(input, AST.defaultParseOptions) + if (Exit.isFailure(exit)) { + const issue = Cause.findError(exit.cause) + if (Result.isFailure(issue)) { + throw Cause.squash(issue.failure) + } + throw new Error(issue.success.toString(), { cause: issue.success }) + } +} + +/** + * Creates an effectful decoder for `unknown` input. + * + * **When to use** + * + * Use when decoding untyped boundary input while preserving decoding failures, + * effectful transformations, and service requirements in an `Effect`. + * + * **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?: AST.ParseOptions +): (input: unknown, options?: AST.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 the input is already typed as the schema's `Encoded` type and + * decoding should stay in `Effect`, including parse failures and required + * decoding services. + * + * **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?: AST.ParseOptions +) => (input: S["Encoded"], options?: AST.ParseOptions) => Effect.Effect = + decodeUnknownEffect + +/** + * Creates a Promise-based decoder for `unknown` input. + * + * **When to use** + * + * Use when decoding untyped input with a service-free schema at a JavaScript + * `Promise` boundary. + * + * **Details** + * + * The returned function resolves with the decoded `Type` on success and rejects + * with a `SchemaIssue.Issue` on decoding failure. + * + * @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?: AST.ParseOptions +): (input: unknown, options?: AST.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 the input is already typed as the schema's `Encoded` type and you + * need a native `Promise` boundary. + * + * **Details** + * + * The returned function resolves with the decoded `Type` on success and rejects + * with a `SchemaIssue.Issue` on decoding failure. + * + * @see {@link decodeUnknownPromise} for untyped input at a `Promise` boundary + * @see {@link decodeEffect} for preserving decoding services and failures in `Effect` + * + * @category decoding + * @since 3.10.0 + */ +export function decodePromise>( + schema: S, + options?: AST.ParseOptions +): (input: S["Encoded"], options?: AST.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 decoding unknown input synchronously and preserving the parser + * outcome as an `Exit` value. + * + * **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. + * + * @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 decodeUnknownOption} for discarding issue details + * @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?: AST.ParseOptions +): (input: unknown, options?: AST.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 to synchronously decode already typed `Encoded` input when you want + * decoding failures returned as `Exit` values. + * + * **Details** + * + * The returned function produces `Exit.Success` with the decoded `Type` or + * `Exit.Failure` with a `SchemaIssue.Issue`. + * + * @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?: AST.ParseOptions +) => (input: S["Encoded"], options?: AST.ParseOptions) => Exit.Exit = decodeUnknownExit + +/** + * Creates a decoder for `unknown` input that returns an `Option` safely. + * + * **When to use** + * + * Use when you need a synchronous yes/no decode from untyped input and do not + * need schema issue details. + * + * **Details** + * + * The returned function produces `Option.some` with the decoded `Type` on success + * or `Option.none` on failure, discarding issue details. + * + * @see {@link decodeOption} for input already typed as the schema's `Encoded` type + * @see {@link decodeUnknownResult} for retaining schema issues as data + * + * @category decoding + * @since 3.10.0 + */ +export function decodeUnknownOption>( + schema: S, + options?: AST.ParseOptions +): (input: unknown, options?: AST.ParseOptions) => Option.Option { + return asOption(decodeUnknownEffect(schema, options)) +} + +/** + * Creates a decoder safely for input already typed as the schema's `Encoded` type, + * returning an `Option`. + * + * **When to use** + * + * Use when the input is already typed as the schema's `Encoded` type and you + * only need to know whether decoding succeeds. + * + * **Details** + * + * The returned function produces `Option.some` with the decoded `Type` on success + * or `Option.none` on failure, discarding issue details. + * + * @see {@link decodeUnknownOption} for untyped input with the same yes/no result shape + * @see {@link decodeResult} for retaining schema issues as data + * + * @category decoding + * @since 3.10.0 + */ +export const decodeOption: >( + schema: S, + options?: AST.ParseOptions +) => (input: S["Encoded"], options?: AST.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 schema issues 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. Schema issues become `Result.fail`, but async + * decoding or defects can still throw. + * + * @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?: AST.ParseOptions +): (input: unknown, options?: AST.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 the input is already typed as the schema's `Encoded` type and you + * want schema decoding failures represented as `Result.fail`. + * + * **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 schema issues, but async + * decoding or other non-schema failures can still throw. + * + * @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?: AST.ParseOptions +) => (input: S["Encoded"], options?: AST.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 be reported by throwing. + * + * **Details** + * + * The returned function returns the decoded `Type` on success and throws an + * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure. + * + * @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?: AST.ParseOptions +): (input: unknown, options?: AST.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 be reported by throwing an `Error`. + * + * **Details** + * + * The returned function returns the decoded `Type` on success and throws an + * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure. + * + * @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?: AST.ParseOptions +) => (input: S["Encoded"], options?: AST.ParseOptions) => S["Type"] = decodeUnknownSync + +/** + * Creates an effectful encoder for `unknown` input. + * + * **When to use** + * + * Use when encoding untyped boundary input and preserving encoding failures and + * service requirements in `Effect` is the desired result shape. + * + * **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?: AST.ParseOptions +): (input: unknown, options?: AST.ParseOptions) => Effect.Effect { + const parser = run(AST.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 to encode values already typed as the schema's decoded `Type` when + * encoding should preserve service requirements and return failures in an + * `Effect`. + * + * **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?: AST.ParseOptions +) => (input: S["Type"], options?: AST.ParseOptions) => Effect.Effect = + encodeUnknownEffect + +/** + * Creates a Promise-based encoder for `unknown` input. + * + * **When to use** + * + * Use to encode untrusted or dynamically typed values at a `Promise` boundary + * when the schema has no encoding service requirements. + * + * **Details** + * + * The returned function resolves with the schema's `Encoded` value on success and + * rejects with a `SchemaIssue.Issue` on encoding failure. + * + * @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?: AST.ParseOptions +): (input: unknown, options?: AST.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 need a `Promise`-returning encoder for values already typed as + * the schema's decoded `Type`, such as at a JavaScript `Promise` interop + * boundary. + * + * **Details** + * + * The returned function resolves with the schema's `Encoded` value on success and + * rejects with a `SchemaIssue.Issue` on encoding failure. + * + * @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?: AST.ParseOptions +) => (input: S["Type"], options?: AST.ParseOptions) => Promise = encodeUnknownPromise + +/** + * Creates a synchronous encoder for `unknown` input that reports failure safely + * as an `Exit`. + * + * **When to use** + * + * Use to encode unknown input synchronously when you want the encoded value or + * schema issue represented as an `Exit`. + * + * **Details** + * + * The returned function produces `Exit.Success` with the schema's `Encoded` value + * or `Exit.Failure` with a `SchemaIssue.Issue`. + * + * @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?: AST.ParseOptions +): (input: unknown, options?: AST.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 to synchronously encode already typed schema values when you want encoding + * failures returned as `Exit` values. + * + * **Details** + * + * The returned function produces `Exit.Success` with the schema's `Encoded` value + * or `Exit.Failure` with a `SchemaIssue.Issue`. + * + * @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?: AST.ParseOptions +) => (input: S["Type"], options?: AST.ParseOptions) => Exit.Exit = encodeUnknownExit + +/** + * Creates an encoder for `unknown` input that returns an `Option` safely. + * + * **When to use** + * + * Use when encoding untyped input and you want a synchronous `Option` result + * that keeps the encoded value on success but discards issue details on failure. + * + * **Details** + * + * The returned function produces `Option.some` with the schema's `Encoded` value + * on success or `Option.none` on failure, discarding issue details. + * + * @see {@link encodeOption} for input already typed as the schema's decoded `Type` + * @see {@link encodeUnknownResult} for retaining schema issues as data + * + * @category encoding + * @since 3.10.0 + */ +export function encodeUnknownOption>( + schema: S, + options?: AST.ParseOptions +): (input: unknown, options?: AST.ParseOptions) => Option.Option { + return asOption(encodeUnknownEffect(schema, options)) +} + +/** + * Creates an encoder safely for input already typed as the schema's decoded `Type`, + * returning an `Option`. + * + * **When to use** + * + * Use when encoding values that are already typed as the schema's decoded + * `Type` and an `Option` result is the desired success/failure boundary. + * + * **Details** + * + * The returned function produces `Option.some` with the schema's `Encoded` value + * on success or `Option.none` on failure, discarding issue details. + * + * @see {@link encodeUnknownOption} for untyped input with the same yes/no result shape + * @see {@link encodeResult} for retaining schema issues as data + * + * @category encoding + * @since 3.10.0 + */ +export const encodeOption: >( + schema: S, + options?: AST.ParseOptions +) => (input: S["Type"], options?: AST.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 schema issues 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. + * + * @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?: AST.ParseOptions +): (input: unknown, options?: AST.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 the input is already typed as the schema's decoded `Type` and + * encoding failures should be returned as a `Result` instead of thrown or run in + * `Effect`. + * + * **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. + * + * @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?: AST.ParseOptions +) => (input: S["Type"], options?: AST.ParseOptions) => Result.Result = encodeUnknownResult + +/** + * Creates a synchronous encoder for `unknown` input. + * + * **When to use** + * + * Use when encoding values from untyped input in synchronous code and treating + * encoding failures as thrown errors is the desired boundary. + * + * **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. + * + * @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?: AST.ParseOptions +): (input: unknown, options?: AST.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 be reported by throwing an `Error`. + * + * **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. + * + * @see {@link encodeUnknownSync} for unknown input with the same throwing boundary + * @see {@link encodeOption} for discarding failure details + * @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?: AST.ParseOptions +) => (input: S["Type"], options?: AST.ParseOptions) => S["Encoded"] = encodeUnknownSync + +const mergeParseOptions = ( + options: AST.ParseOptions, + overrideOptions: AST.ParseOptions | undefined +): AST.ParseOptions => overrideOptions === undefined ? options : { ...options, ...overrideOptions } + +/** @internal */ +export function run(ast: AST.AST) { + const parser = recur(ast) + return (input: unknown, options?: AST.ParseOptions): Effect.Effect => + Effect.flatMapEager(parser(Option.some(input), options ?? AST.defaultParseOptions), (oa) => { + if (oa._tag === "None") { + return Effect.fail(new Issue.InvalidValue(oa)) + } + return Effect.succeed(oa.value as T) + }) +} + +function asPromise( + parser: (input: E, options?: AST.ParseOptions) => Effect.Effect +): (input: E, options?: AST.ParseOptions) => Promise { + return (input: E, options?: AST.ParseOptions) => Effect.runPromise(parser(input, options)) +} + +function asExit( + parser: (input: E, options?: AST.ParseOptions) => Effect.Effect +): (input: E, options?: AST.ParseOptions) => Exit.Exit { + return (input: E, options?: AST.ParseOptions) => Effect.runSyncExit(parser(input, options) as any) +} + +/** @internal */ +export function asOption( + parser: (input: E, options?: AST.ParseOptions) => Effect.Effect +): (input: E, options?: AST.ParseOptions) => Option.Option { + const parserExit = asExit(parser) + return (input: E, options?: AST.ParseOptions) => Exit.getSuccess(parserExit(input, options)) +} + +function asResult( + parser: (input: E, options?: AST.ParseOptions) => Effect.Effect +): (input: E, options?: AST.ParseOptions) => Result.Result { + const parserExit = asExit(parser) + return (input: E, options?: AST.ParseOptions) => { + const exit = parserExit(input, options) + if (Exit.isSuccess(exit)) { + return Result.succeed(exit.value) + } + const error = Cause.findError(exit.cause) + if (Result.isFailure(error)) { + throw Cause.squash(error.failure) + } + return Result.fail(error.success) + } +} + +function asSync( + parser: (input: E, options?: AST.ParseOptions) => Effect.Effect +): (input: E, options?: AST.ParseOptions) => T { + return (input: E, options?: AST.ParseOptions) => + Effect.runSync( + Effect.mapErrorEager( + parser(input, options), + (issue) => new Error(issue.toString(), { cause: issue }) + ) as any + ) +} + +/** @internal */ +export interface Parser { + (input: Option.Option, options: AST.ParseOptions): Effect.Effect, Issue.Issue, any> +} + +const recur = memoize( + (ast: AST.AST): Parser => { + let parser: Parser + const astOptions = InternalAnnotations.resolve(ast)?.["parseOptions"] + if (!ast.context && !ast.encoding && !ast.checks) { + return (ou, options) => { + parser ??= ast.getParser(recur) + if (astOptions) { + options = { ...options, ...astOptions } + } + return parser(ou, options) + } + } + const isStructural = AST.isArrays(ast) || AST.isObjects(ast) || + (AST.isDeclaration(ast) && ast.typeParameters.length > 0) + return (ou, options) => { + if (astOptions) { + options = { ...options, ...astOptions } + } + const encoding = ast.encoding + let srou: Effect.Effect, Issue.Issue, unknown> | undefined + if (encoding) { + const links = encoding + const len = links.length + 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 = Effect.mapErrorEager(srou!, (issue) => new Issue.Encoding(ast, ou, issue)) + } + + parser ??= ast.getParser(recur) + let sroa = srou ? Effect.flatMapEager(srou, (ou) => parser(ou, options)) : parser(ou, options) + + if (ast.checks && !options?.disableChecks) { + const checks = ast.checks + if (options?.errors === "all" && isStructural && Option.isSome(ou)) { + sroa = Effect.catchEager(sroa, (issue) => { + const issues: Array = [] + AST.collectIssues( + checks.filter((check) => check.annotations?.[AST.STRUCTURAL_ANNOTATION_KEY]), + ou.value, + issues, + ast, + options + ) + const out: Issue.Issue = Arr.isArrayNonEmpty(issues) + ? issue._tag === "Composite" && issue.ast === ast + ? new Issue.Composite(ast, issue.actual, [...issue.issues, ...issues]) + : new Issue.Composite(ast, ou, [issue, ...issues]) + : issue + return Effect.fail(out) + }) + } + sroa = Effect.flatMapEager(sroa, (oa) => { + if (Option.isSome(oa)) { + const value = oa.value + const issues: Array = [] + + AST.collectIssues(checks, value, issues, ast, options) + + if (Arr.isArrayNonEmpty(issues)) { + return Effect.fail(new Issue.Composite(ast, oa, issues)) + } + } + return Effect.succeed(oa) + }) + } + + return sroa + } + } +) diff --git a/.repos/effect-smol/packages/effect/src/SchemaRepresentation.ts b/.repos/effect-smol/packages/effect/src/SchemaRepresentation.ts new file mode 100644 index 00000000000..9088d0df806 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/SchemaRepresentation.ts @@ -0,0 +1,3906 @@ +/** + * Serializable intermediate representation (IR) of Effect Schema types. + * + * `SchemaRepresentation` sits between the internal `SchemaAST` and external + * formats (JSON Schema, generated TypeScript code, serialized JSON). A + * {@link Representation} is a discriminated union describing the *shape* of a + * schema — its types, checks, annotations, and references — in a form that + * can be round-tripped through JSON and used for code generation. + * + * ## Mental model + * + * - **Representation**: A tagged union (`_tag`) of all supported schema shapes: + * primitives, literals, objects, arrays, unions, declarations, references, + * and suspensions. + * - **Document**: A single {@link Representation} paired with a map of named + * {@link References} (analogous to JSON Schema `$defs`). + * - **MultiDocument**: Like `Document` but holds one or more representations + * sharing the same references. + * - **Check / Filter / FilterGroup**: Validation constraints (min length, + * pattern, integer, etc.) attached to types that support them. + * - **Meta types**: Typed metadata for checks on each category — e.g. + * {@link StringMeta}, {@link NumberMeta}, {@link ArraysMeta}. + * - **Reviver**: A callback used by {@link toSchema} and {@link toCodeDocument} + * to handle `Declaration` nodes (custom types like `Option`, `Date`, etc.). + * - **Code / CodeDocument**: Output of {@link toCodeDocument} — TypeScript + * source strings for runtime schemas and their type-level counterparts. + * + * ## Common tasks + * + * - Convert a Schema AST to a Document → {@link fromAST} + * - Convert multiple ASTs to a MultiDocument → {@link fromASTs} + * - Reconstruct a runtime Schema from a Document → {@link toSchema} + * - Convert a Document to JSON Schema → {@link toJsonSchemaDocument} + * - Convert a MultiDocument to JSON Schema → {@link toJsonSchemaMultiDocument} + * - Parse a JSON Schema document into a Document → {@link fromJsonSchemaDocument} + * - Parse a JSON Schema multi-document → {@link fromJsonSchemaMultiDocument} + * - Generate TypeScript code from a MultiDocument → {@link toCodeDocument} + * - Serialize/deserialize a Document as JSON → {@link DocumentFromJson} + * - Serialize/deserialize a MultiDocument as JSON → {@link MultiDocumentFromJson} + * - Wrap a Document as a MultiDocument → {@link toMultiDocument} + * + * ## Gotchas + * + * - `Declaration` nodes require a {@link Reviver} to reconstruct complex types + * (e.g. `Option`, `Date`). Without one, `toSchema` falls back to the + * declaration's `encodedSchema`. Use {@link toSchemaDefaultReviver} for + * built-in Effect types. + * - `Reference` nodes are resolved against the `references` map in the + * `Document`. An unresolvable `$ref` throws at runtime. + * - `Suspend` wraps a single `thunk` representation; it is used for recursive + * schemas. Circular references are handled by lazy resolution in + * {@link toSchema}. + * - The `$`-prefixed exports (e.g. {@link $Representation}, {@link $Document}) + * are Schema codecs for the representation types themselves — use them to + * validate or encode/decode representation data, not application data. + * + * ## Quickstart + * + * **Example** (Round-trip through JSON) + * + * ```ts + * import { Schema, SchemaRepresentation } from "effect" + * + * const Person = Schema.Struct({ + * name: Schema.String, + * age: Schema.Int + * }) + * + * // Schema AST → Document + * const doc = SchemaRepresentation.fromAST(Person.ast) + * + * // Document → JSON Schema + * const jsonSchema = SchemaRepresentation.toJsonSchemaDocument(doc) + * + * // Document → runtime Schema + * const reconstructed = SchemaRepresentation.toSchema(doc) + * ``` + * + * ## See also + * + * - {@link Representation} — the core tagged union + * - {@link Document} — single-schema container + * - {@link fromAST} — entry point from Schema AST + * - {@link toSchema} — reconstruct a runtime Schema + * - {@link toCodeDocument} — generate TypeScript code + * + * @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 * as Option from "./Option.ts" +import * as Predicate from "./Predicate.ts" +import * as Rec from "./Record.ts" +import * as Schema from "./Schema.ts" +import type * as AST from "./SchemaAST.ts" +import * as Getter 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. + * + * **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" + | "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: Getter.passthrough(), + encode: Getter.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 $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, + $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 and need its representation. + * + * **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: AST.AST) => Document = InternalRepresentation.fromAST + +/** + * Converts one or more Schema ASTs into a {@link MultiDocument}. + * + * **When to use** + * + * Use when you have multiple schemas that may share 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 [AST.AST, ...Array]) => MultiDocument = InternalRepresentation.fromASTs + +/** + * Schema that decodes a {@link Document} from JSON and encodes it back. + * + * **When to use** + * + * Use with `Schema.decodeUnknownSync` or `Schema.encodeSync` to serialize + * and deserialize documents. + * + * **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 an API expects a `MultiDocument` but you only have a single + * `Document`. + * + * @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 passing this as `options.reviver` to {@link toSchema} to reconstruct + * schemas that use these types. + * + * **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 + case "ErrorWithStack": + return Schema.ErrorWithStack + 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]) + 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 representation and need a + * working 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): AST.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): AST.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 "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 to produce a standard JSON Schema from an Effect Schema + * representation. + * + * **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 have multiple schemas sharing references. + * + * @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 to produce source code for Schema definitions, such as in codegen + * tools. + * + * **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 "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}(${ca})` + + 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 to import external JSON Schemas into the Effect representation + * system. + * + * **Details** + * + * `options.onEnter` is an optional hook called on each JSON Schema node before + * processing, allowing pre-transformation. + * + * **Gotchas** + * + * 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 to import multiple JSON Schemas sharing definitions. + * + * **Details** + * + * `options.onEnter` is an optional hook called on each JSON Schema node before + * processing. + * + * **Gotchas** + * + * 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 visited: Set + const references: Record = {} + + type Slot = { + // 0 = not started, 1 = building, 2 = done + state: 0 | 1 | 2 + value: Exclude | undefined + } + + const slots = new Map() + + 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 + } + slots.set(identifier, slot) + return slot + } + + function resolveReference($ref: string): Exclude { + const definition = document.definitions[$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. + throw new Error(`Circular reference detected: ${$ref}`) + } + + // First time: build it. + slot.state = 1 + const value = recur(definition) + + slot.value = value._tag === "Reference" ? resolveReference(value.$ref) : value + slot.state = 2 + return slot.value + } + + Object.entries(document.definitions).forEach(([identifier, definition]) => { + visited = new Set([identifier]) + references[identifier] = recur(definition) + }) + + visited = new Set() + const representations = Arr.map(document.schemas, recur) + 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 (visited.has($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": + switch (b._tag) { + case "Reference": + return combine(a, resolveReference(b.$ref)) + default: + return { ...b, ...combineAnnotations(a.annotations, b.annotations) } + } + case "Null": + switch (b._tag) { + case "Unknown": + case "Null": + return { ...a, ...combineAnnotations(a.annotations, b.annotations) } + case "Union": + return combine(b, a) + case "Reference": + return combine(a, resolveReference(b.$ref)) + default: + return never + } + case "String": + switch (b._tag) { + case "Unknown": + return { ...a, ...combineAnnotations(a.annotations, b.annotations) } + case "String": { + const checks = combineChecks(a.checks, b.checks, b.annotations) + return { + _tag: "String", + checks: checks ?? a.checks, + ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) + } + } + case "Literal": + return typeof b.literal === "string" ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } : never + case "Union": + return combine(b, a) + case "Reference": + return combine(a, resolveReference(b.$ref)) + default: + return never + } + case "Number": + switch (b._tag) { + case "Unknown": + return { ...a, ...combineAnnotations(a.annotations, b.annotations) } + case "Number": { + const checks = combineNumberChecks(a.checks, b.checks, b.annotations) + return { + _tag: "Number", + checks: checks ?? a.checks, + ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) + } + } + case "Literal": + return typeof b.literal === "number" ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } : never + case "Union": + return combine(b, a) + case "Reference": + return combine(a, resolveReference(b.$ref)) + default: + return never + } + case "Boolean": + switch (b._tag) { + case "Unknown": + return { ...a, ...combineAnnotations(a.annotations, b.annotations) } + case "Boolean": + return { _tag: "Boolean", ...combineAnnotations(a.annotations, b.annotations) } + case "Literal": + return typeof b.literal === "boolean" + ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } + : never + case "Union": + return combine(b, a) + case "Reference": + return combine(a, resolveReference(b.$ref)) + default: + return never + } + case "Literal": + switch (b._tag) { + case "Unknown": + return { ...a, ...combineAnnotations(a.annotations, b.annotations) } + case "Literal": + return a.literal === b.literal + ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } + : never + case "String": + return typeof a.literal === "string" ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } : never + case "Number": + return typeof a.literal === "number" ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } : never + case "Boolean": + return typeof a.literal === "boolean" + ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } + : never + case "Union": + return combine(b, a) + case "Reference": + return combine(a, resolveReference(b.$ref)) + default: + return never + } + case "Arrays": + switch (b._tag) { + case "Unknown": + return { ...a, ...combineAnnotations(a.annotations, b.annotations) } + case "Arrays": { + const checks = combineArraysChecks(a.checks, b.checks, b.annotations) + return { + _tag: "Arrays", + elements: combineElements(a.elements, b.elements), + rest: combineRest(a.rest, b.rest), + checks: checks ?? a.checks, + ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) + } + } + case "Union": + return combine(b, a) + case "Reference": + return combine(a, resolveReference(b.$ref)) + default: + return never + } + case "Objects": + switch (b._tag) { + case "Unknown": + return { ...a, ...combineAnnotations(a.annotations, b.annotations) } + case "Objects": { + 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) + } + } + case "Union": + return combine(b, a) + case "Reference": + return combine(a, resolveReference(b.$ref)) + default: + return never + } + case "Union": { + switch (b._tag) { + case "Unknown": + return { ...a, ...combineAnnotations(a.annotations, b.annotations) } + default: { + 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) + } + } + } + } + } + } + + 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 combineElements(a: ReadonlyArray, b: ReadonlyArray): Array { + const len = Math.max(a.length, b.length) + let out: Array = [] + for (let i = 0; i < len; i++) { + out.push({ + isOptional: a[i].isOptional && b[i].isOptional, + type: combine(a[i].type, b[i].type) + }) + } + if (a.length > len) { + out = [...out, ...a.slice(len)] + } else if (b.length > len) { + out = [...out, ...b.slice(len)] + } + return out + } + + function combineRest(a: ReadonlyArray, b: ReadonlyArray): Array { + const len = Math.max(a.length, b.length) + let out: Array = [] + for (let i = 0; i < len; i++) { + out.push(combine(a[i], b[i])) + } + if (a.length > len) { + out = [...out, ...a.slice(len)] + } else if (b.length > len) { + out = [...out, ...b.slice(len)] + } + return out + } + + 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 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 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 AST.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-smol/packages/effect/src/SchemaTransformation.ts b/.repos/effect-smol/packages/effect/src/SchemaTransformation.ts new file mode 100644 index 00000000000..20ce8afd079 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/SchemaTransformation.ts @@ -0,0 +1,1815 @@ +/** + * Bidirectional transformations for the Effect Schema system. + * + * A `Transformation` pairs a decode `Getter` and an encode `Getter` into a + * single bidirectional value, used by `Schema.decodeTo`, `Schema.encodeTo`, + * `Schema.decode`, `Schema.encode`, and `Schema.link` to define how values + * are converted between encoded and decoded representations. A `Middleware` + * is the effect-level equivalent — it wraps the entire parsing `Effect` + * pipeline rather than individual values. + * + * ## Mental model + * + * - **Transformation**: A pair of `Getter`s (decode + encode) that convert + * individual values bidirectionally. `T` is the decoded (Type) side, `E` is + * the encoded side. `RD`/`RE` are required Effect services. + * - **Middleware**: Like `Transformation`, but each direction receives the full + * parsing `Effect` and can intercept, retry, or modify the pipeline. + * - **Getter**: A single-direction transform `Option → Effect, Issue, R>` + * (see `SchemaGetter`). + * - **flip()**: Swaps decode and encode, turning a `Transformation` into + * `Transformation`. + * - **compose()**: Chains two transformations left-to-right on the decode side + * and right-to-left on the encode side. + * - **passthrough**: The identity transformation — no conversion in either + * direction. + * + * ## Common tasks + * + * - Convert values purely (sync, infallible) → {@link transform} + * - Convert values with possible failure → {@link transformOrFail} + * - Handle optional/missing keys → {@link transformOptional} + * - Build from existing Getters → {@link make} + * - No-op identity transformation → {@link passthrough} + * - Subtype/supertype coercion → {@link passthroughSupertype}, {@link passthroughSubtype} + * - Trim/case strings → {@link trim}, {@link toLowerCase}, {@link toUpperCase}, {@link capitalize}, {@link uncapitalize}, {@link snakeToCamel} + * - Parse key-value strings → {@link splitKeyValue} + * - Coerce string ↔ number/bigint → {@link numberFromString}, {@link bigintFromString} + * - Coerce string ↔ Date/Duration → {@link dateFromString}, {@link durationFromString} + * - Decode durations → {@link durationFromNanos}, {@link durationFromMillis} + * - Wrap nullable/optional as Option → {@link optionFromNullOr}, {@link optionFromOptionalKey}, {@link optionFromOptional} + * - Parse URLs → {@link urlFromString} + * - Base64 ↔ Uint8Array → {@link uint8ArrayFromBase64String} + * - Base64 ↔ string → {@link stringFromBase64String} + * - URI component ↔ string → {@link stringFromUriComponent} + * - JSON string ↔ unknown → {@link fromJsonString} + * - FormData/URLSearchParams ↔ unknown → {@link fromFormData}, {@link fromURLSearchParams} + * - Check if a value is a Transformation → {@link isTransformation} + * + * ## Gotchas + * + * - `Transformation` operates on individual values; `Middleware` wraps the + * entire parsing Effect. Choose accordingly. + * - `passthrough` requires `T === E` by default. Use `{ strict: false }` to + * bypass, or use {@link passthroughSupertype} / {@link passthroughSubtype}. + * - String transformations like `trim`, `toLowerCase`, and `toUpperCase` use + * `passthrough` on the encode side — they are lossy and do not round-trip. + * - `durationFromNanos` encode can fail if the Duration cannot be represented + * as a `bigint`. + * + * ## Quickstart + * + * **Example** (Defining a custom transformation with Schema.decodeTo) + * + * ```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 also + * + * - {@link Transformation} — the core bidirectional transformation class + * - {@link Middleware} — effect-pipeline-level transformation + * - {@link transform} — most common constructor + * - {@link passthrough} — identity transformation + * + * @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 { formatDate } from "./Formatter.ts" +import * as Option from "./Option.ts" +import * as Predicate from "./Predicate.ts" +import type * as AST from "./SchemaAST.ts" +import * as Getter from "./SchemaGetter.ts" +import * as Issue from "./SchemaIssue.ts" + +/** + * Middleware that wraps the entire parsing `Effect` pipeline for both + * decode and encode directions. + * + * **When to use** + * + * Use when you need to catch or recover from parsing errors (e.g. `Schema.catchDecoding`). + * - You need to run side effects around the parsing pipeline. + * - You need access to 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, Issue.Issue, RDE>, + options: AST.ParseOptions + ) => Effect.Effect, Issue.Issue, RDT> + readonly encode: ( + effect: Effect.Effect, Issue.Issue, RET>, + options: AST.ParseOptions + ) => Effect.Effect, Issue.Issue, REE> + + constructor( + decode: ( + effect: Effect.Effect, Issue.Issue, RDE>, + options: AST.ParseOptions + ) => Effect.Effect, Issue.Issue, RDT>, + encode: ( + effect: Effect.Effect, Issue.Issue, RET>, + options: AST.ParseOptions + ) => Effect.Effect, Issue.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 to define 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: Getter.Getter + readonly encode: Getter.Getter + + constructor( + decode: Getter.Getter, + encode: Getter.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 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 `Getter` instances and want to pair them. + * - 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: Getter.Getter + readonly encode: Getter.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 the transformation can fail (e.g. parsing, validation). + * - The transformation requires 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: AST.ParseOptions) => Effect.Effect + readonly encode: (t: T, options: AST.ParseOptions) => Effect.Effect +}): Transformation { + return new Transformation( + Getter.transformOrFail(options.decode), + Getter.transformOrFail(options.encode) + ) +} + +/** + * Creates a `Transformation` from pure (sync, infallible) decode and encode + * functions. + * + * **When to use** + * + * Use when the conversion cannot fail. + * - No Effect services are needed. + * + * **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( + Getter.transform(options.decode), + Getter.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 to produce or consume `Option.None` to represent 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( + Getter.transformOptional(options.decode), + Getter.transformOptional(options.encode) + ) +} + +/** + * Transforms strings by trimming whitespace on decode. + * Encode is passthrough (no change). + * + * **When to use** + * + * Use to normalize user input by stripping leading/trailing whitespace. + * + * **Details** + * + * - Decode: applies `String.prototype.trim()`. + * - Encode: passthrough (returns the string unchanged). + * - 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( + Getter.trim(), + Getter.passthrough() + ) +} + +/** + * Transforms strings by converting snake_case to camelCase + * on decode and camelCase to snake_case on encode. + * + * **When to use** + * + * Use to convert API field names between snake_case and camelCase conventions. + * + * **Details** + * + * - Decode: `"my_field_name"` → `"myFieldName"`. + * - Encode: `"myFieldName"` → `"my_field_name"`. + * - Round-trippable for standard snake_case/camelCase. + * + * **Example** (Snake to camel conversion) + * + * ```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( + Getter.snakeToCamel(), + Getter.camelToSnake() + ) +} + +/** + * Transforms strings by lowercasing on decode. + * Encode is passthrough. + * + * **When to use** + * + * Use to normalize strings to lowercase (e.g. email addresses). + * + * **Details** + * + * - Decode: applies `String.prototype.toLowerCase()`. + * - Encode: passthrough. + * - 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( + Getter.toLowerCase(), + Getter.passthrough() + ) +} + +/** + * Transforms strings by uppercasing on decode. + * Encode is passthrough. + * + * **When to use** + * + * Use to normalize strings to uppercase (e.g. country codes). + * + * **Details** + * + * - Decode: applies `String.prototype.toUpperCase()`. + * - Encode: passthrough. + * - 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( + Getter.toUpperCase(), + Getter.passthrough() + ) +} + +/** + * Transforms strings by capitalizing the first character on + * decode. Encode is passthrough. + * + * **When to use** + * + * Use to normalize display names or titles. + * + * **Details** + * + * - Decode: uppercases the first character, leaves the rest unchanged. + * - Encode: 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( + Getter.capitalize(), + Getter.passthrough() + ) +} + +/** + * Transforms strings by lowercasing the first character on + * decode. Encode is passthrough. + * + * **When to use** + * + * Use to normalize identifiers or field names. + * + * **Details** + * + * - Decode: lowercases the first character, leaves the rest unchanged. + * - Encode: 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( + Getter.uncapitalize(), + Getter.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 to parse query-string-like or config-file-like strings into records. + * + * **Details** + * + * - Decode: splits the string by `separator` (default `","`) into pairs, + * then splits each pair by `keyValueSeparator` (default `"="`). + * - Encode: joins the record back into a string using the same separators. + * - Round-trippable when keys and values don't 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( + Getter.splitKeyValue(options), + Getter.joinKeyValue(options) + ) +} + +const passthrough_ = new Transformation( + Getter.passthrough(), + Getter.passthrough() +) + +/** + * Transforms values by returning the input unchanged in both + * directions. + * + * **When to use** + * + * Use when connecting two schemas that share the same type with no conversion. + * - As a placeholder when `Schema.decodeTo` requires a transformation but + * no actual conversion is needed. + * + * **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 the runtime value is unchanged but the decoded side should be + * narrower than the encoded side. + * + * **Details** + * + * Both decode and encode are no-ops and return a shared singleton + * transformation. + * + * **Example** (Supertype passthrough) + * + * ```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 narrowing: the encoded side is more specific than the decoded side. + * + * **Details** + * + * - Both decode and encode are no-ops (same as {@link passthrough}). + * - Returns a shared singleton instance. + * + * **Example** (Subtype passthrough) + * + * ```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 to parse numeric strings from APIs, form data, or URL parameters. + * + * **Details** + * + * - Decode: coerces the string to a number (like `Number(s)`). + * - Encode: coerces the number to a string (like `String(n)`). + * - Does not validate that the result is finite — combine with + * `Schema.Finite` or `Schema.Int` for stricter checks. + * + * **Example** (Number from string) + * + * ```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( + Getter.Number(), + Getter.String() +) + +/** + * Decodes a `string` into a `bigint` and encodes a `bigint` back to a + * `string`. + * + * **When to use** + * + * Use to parse large integer strings (e.g. database IDs, blockchain values). + * + * **Details** + * + * - Decode: coerces the string to a bigint (like `BigInt(s)`). + * - Encode: coerces the bigint to a string (like `String(n)`). + * - Fails on decode if the string is not a valid bigint representation. + * + * **Example** (BigInt from string) + * + * ```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( + Getter.BigInt(), + Getter.String() +) + +/** + * Decodes a `string` into a `Date` and encodes a `Date` back to a `string`. + * + * **When to use** + * + * Use to parse ISO 8601 date strings from APIs or user input. + * + * **Details** + * + * - Decode: creates a `Date` from the string (like `new Date(s)`). + * - Encode: converts the `Date` to an ISO string (like `date.toISOString()`), + * returning `"Invalid Date"` for invalid dates. + * + * **Example** (Date from string) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromString) + * ) + * ``` + * + * @see {@link numberFromString} + * @see {@link dateTimeUtcFromString} + * + * @category Coercions + * @since 4.0.0 + */ +export const dateFromString: Transformation = new Transformation( + Getter.Date(), + Getter.transform(formatDate) +) + +/** + * Decodes a `string` into a `Duration` and encodes a `Duration` back to a + * parseable `string`. + * + * **When to use** + * + * Use to parse human-readable duration strings from APIs, config, or user input. + * + * **Details** + * + * - Decode: accepts any string that `Duration.fromInput` can parse, including + * `"Infinity"` and `"-Infinity"`. + * - Encode: returns `String(duration)`, producing strings like `"2000 millis"` + * or `"10 nanos"` that round-trip through the parser. + * + * **Example** (Duration from string) + * + * ```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 Issue.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 working with nanosecond-precision timestamps or intervals. + * + * **Details** + * + * - Decode: always succeeds, creating a Duration from nanoseconds. + * - Encode: fails with `InvalidValue` if the Duration cannot be represented + * as a `bigint` (e.g. `Duration.infinity`). + * + * **Example** (Duration from nanoseconds) + * + * ```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 Issue.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 use this for 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** (Duration from milliseconds) + * + * ```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) +}) + +/** @internal */ +export const errorFromErrorJsonEncoded = (options?: { + readonly includeStack?: boolean | undefined +}): Transformation => + transform({ + decode: (i) => { + const err = new Error(i.message) + if (typeof i.name === "string" && i.name !== "Error") err.name = i.name + if (typeof i.stack === "string") err.stack = i.stack + return err + }, + encode: (a) => { + const e: { + message: string + name?: string + stack?: string + } = { + name: a.name, + message: a.message + } + if (options?.includeStack && typeof a.stack === "string") { + e.stack = a.stack + } + return e + } + }) + +/** + * Decodes `T | null` into `Option` and encodes `Option` back to + * `T | null`. + * + * **When to use** + * + * Use to convert nullable API fields to `Option`. + * + * **Details** + * + * - Decode: `null` → `Option.none()`, non-null → `Option.some(value)`. + * - Encode: `Option.none()` → `null`, `Option.some(value)` → `value`. + * - Pure and synchronous. + * + * **Example** (Option from nullable) + * + * ```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 to convert undefined-or API fields to `Option`. + * + * **Details** + * + * - Decode: `undefined` → `Option.none()`, non-undefined → `Option.some(value)`. + * - Encode: `Option.none()` → `undefined`, `Option.some(value)` → `value`. + * - Pure and synchronous. + * + * **Example** (Option from undefined-or) + * + * ```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 to convert nullish API fields to `Option` when both `null` and + * `undefined` represent absence. + * + * **Details** + * + * - Decode: `null` or `undefined` → `Option.none()`, otherwise → `Option.some(value)`. + * - Encode: `Option.none()` → `null` or `undefined` (per `options.onNoneEncoding`), + * `Option.some(value)` → `value`. + * - Pure and synchronous. + * + * **Example** (Option from nullish, 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 to convert optional struct keys (declared with `Schema.optionalKey`) to + * `Option` values. + * + * **Details** + * + * - Decode: absent key (`None`) → `Some(None)`, present key (`Some(v)`) → `Some(Some(v))`. + * - Encode: `Some(None)` → `None` (omit key), `Some(Some(v))` → `Some(v)`. + * - Uses `transformOptional` under the hood. + * + * **Example** (Optional key to 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 to convert optional (possibly `undefined`) values to `Option`. + * + * **Details** + * + * - Decode: absent or `undefined` → `Some(None)`, present → `Some(Some(v))`. + * - Encode: `Some(None)` → `None` (omit), `Some(Some(v))` → `Some(v)`. + * - Uses `transformOptional` under the hood; filters out `undefined` on decode. + * + * **Example** (Optional value to 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 to parse URL strings from user input or API responses. + * + * **Details** + * + * - Decode: calls `new URL(s)`. Fails with `InvalidValue` if the string + * is not a valid URL. + * - Encode: returns `url.href`. + * + * **Example** (URL from string) + * + * ```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) => + Effect.try({ + try: () => new URL(s), + catch: () => new Issue.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 to parse decimal number strings from APIs or user input. + * + * **Details** + * + * - Decode: calls `BigDecimal.fromString(s)`. Fails with `InvalidValue` if the + * string is not a valid BigDecimal representation. + * - Encode: 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 Issue.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 handling binary data transmitted as Base64 strings (e.g. file uploads, + * API payloads). + * + * **Details** + * + * - Decode: parses the Base64 string into bytes. + * - Encode: encodes the byte array as a Base64 string. + * + * **Example** (Uint8Array from Base64) + * + * ```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( + Getter.decodeBase64(), + Getter.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 handling text data transmitted as Base64 strings. + * + * **Details** + * + * - Decode: parses the Base64 string into a UTF-8 string. + * - Encode: encodes the string as a Base64 string. + * + * **Example** (String from Base64) + * + * ```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( + Getter.decodeBase64String(), + Getter.encodeBase64() +) + +/** + * Decodes a base64 (URL) encoded `string` into a UTF-8 `string` and encodes it back. + * + * **When to use** + * + * Use when handling text data transmitted as Base64 URL-safe strings. + * + * **Details** + * + * - Decode: parses the Base64 URL string into a UTF-8 string. + * - Encode: encodes the string as a Base64 URL string. + * + * **Example** (String from Base64Url) + * + * ```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( + Getter.decodeBase64UrlString(), + Getter.encodeBase64Url() +) + +/** + * Decodes a hex encoded `string` into a UTF-8 `string` and encodes it back. + * + * **When to use** + * + * Use when handling text data transmitted as hexadecimal strings. + * + * **Details** + * + * - Decode: parses the hex string into a UTF-8 string. + * - Encode: encodes the string as a hex string. + * + * **Example** (String from Hex) + * + * ```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( + Getter.decodeHexString(), + Getter.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 storing structured data in URL query parameters or fragments. + * - Composing with `Schema.parseJson` to round-trip JSON through a URL. + * + * **Details** + * + * - Decode: calls `decodeURIComponent`. Fails if the input contains malformed + * percent-encoding sequences. + * - Encode: calls `encodeURIComponent`. + * + * **Example** (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( + Getter.decodeUriComponent(), + Getter.encodeUriComponent() +) + +/** + * Decodes a JSON string with `JSON.parse` and encodes a value with + * `JSON.stringify`. + * + * **When to use** + * + * Use when you use this for 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( + Getter.parseJson(), + Getter.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 use this for form or multipart payloads where 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( + Getter.decodeFormData(), + Getter.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 use this for query strings where 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( + Getter.decodeURLSearchParams(), + Getter.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 to represent fixed-offset time zones with numeric millisecond offsets in + * schema transformations or JSON codecs. + * + * **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 a schema transformation should 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 Issue.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 schema decoding should 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 Issue.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 to decode date-time strings when the schema value should be 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 dateTimeZonedFromString} for ISO strings that should preserve zoned date-time information + * @see {@link dateFromString} for decoding into JavaScript `Date` + * + * @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 Issue.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 to define 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 Issue.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-smol/packages/effect/src/SchemaUtils.ts b/.repos/effect-smol/packages/effect/src/SchemaUtils.ts new file mode 100644 index 00000000000..5540fac3d0f --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/SchemaUtils.ts @@ -0,0 +1,79 @@ +/** + * Focused schema helpers for patterns that are useful but too specialized for + * the main `Schema` module. This module currently covers the case where a + * native JavaScript or TypeScript class should decode from a plain struct and + * still remain an instance of that class after decoding. + * + * **Mental model** + * + * - {@link getNativeClassSchema} starts from a constructor and a struct schema + * for the encoded shape. + * - Decoding validates the struct, then calls the constructor with the decoded + * fields as one object. + * - Encoding treats the instance as the encoded object, so instance properties + * must line up with the struct fields. + * - The resulting schema preserves class identity through `Schema.instanceOf` + * while retaining a plain-object representation for encoded data. + * + * **Common tasks** + * + * - Add schema support to an existing native class without rewriting it as a + * `Schema.Class`. + * - Decode structured data into a `Data.Error` subclass or another class whose + * constructor accepts a props object. + * - Encode class instances back to the struct shape expected at API, storage, + * or transport boundaries. + * + * **Gotchas** + * + * - Constructors that expect positional arguments are not compatible unless + * they also accept the decoded props object. + * - Private fields or computed getters are not enough for encoding; the + * instance must expose properties compatible with the provided struct schema. + * - Prefer `Schema.Class` or `Schema.ErrorClass` when you control the class + * definition and do not need to adapt an existing constructor. + * + * @since 4.0.0 + */ +import { identity } from "./Function.ts" +import * as Schema from "./Schema.ts" +import * as Transformation 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 = Transformation.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-smol/packages/effect/src/Scope.ts b/.repos/effect-smol/packages/effect/src/Scope.ts new file mode 100644 index 00000000000..2a8f61e17e8 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Scope.ts @@ -0,0 +1,592 @@ +/** + * The `Scope` module manages resource lifetimes by collecting finalizers and + * running them when a scope is closed. It is the low-level mechanism behind + * scoped resource acquisition: acquire work registers cleanup with the current + * scope, and closing the scope releases those resources with the same `Exit` + * that ended the scoped workflow. + * + * Scopes are useful when code needs explicit control over a lifetime boundary, + * for example when building services, sharing a resource across multiple + * effects, or wiring lower-level infrastructure. Most application code can use + * higher-level scoped APIs, but this module exposes the primitives those APIs + * are built from. + * + * **Mental model** + * + * - A {@link Scope} is a mutable lifetime boundary that can accept finalizers + * while open and run them when closed + * - A {@link Closeable} scope can be closed explicitly with {@link close} or + * automatically around an effect with {@link use} + * - Sequential scopes run finalizers one after another in reverse registration + * order; parallel scopes run registered finalizers concurrently + * - {@link addFinalizer} registers cleanup that ignores the closing exit, while + * {@link addFinalizerExit} registers cleanup that can inspect it + * - {@link fork} creates a child scope whose lifetime is connected to a parent + * scope + * + * **Common tasks** + * + * - Create scopes with {@link make} or the lower-level {@link makeUnsafe} + * - Provide an existing scope to an effect with {@link provide} + * - Register cleanup with {@link addFinalizer} or {@link addFinalizerExit} + * - Create child scopes with {@link fork} or {@link forkUnsafe} + * - Close scopes with {@link close}, or run and close with {@link use} + * + * **Gotchas** + * + * - Closing a scope is itself an `Effect`; finalizers run only when that effect + * is executed + * - Adding a finalizer to an already closed scope runs the finalizer + * immediately with the stored exit value + * - The unsafe constructors and closing helpers are for low-level integration; + * prefer the effectful APIs when ordinary Effect code can express the + * lifetime + * + * @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 tags + * @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. + * + * **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. + * + * **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. + * + * **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-smol/packages/effect/src/ScopedCache.ts b/.repos/effect-smol/packages/effect/src/ScopedCache.ts new file mode 100644 index 00000000000..2b3b11c60c2 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/ScopedCache.ts @@ -0,0 +1,835 @@ +/** + * The `ScopedCache` module provides a cache for values that acquire scoped + * resources during lookup. Each cached entry owns a `Scope`, so resources + * created while computing a value stay alive for as long as that entry remains + * cached and are released when the entry is removed. + * + * A `ScopedCache` is itself created inside a scope. Calls to {@link get} run the + * lookup effect on cache misses, share the same in-flight lookup among + * concurrent callers for the same key, and store the resulting exit according + * to a time-to-live policy. Entries can be inserted manually with {@link set}, + * refreshed with {@link refresh}, inspected without triggering lookup with + * {@link getOption}, and removed with {@link invalidate} or + * {@link invalidateAll}. Capacity limits evict the oldest entries. + * + * **Lifecycle notes** + * + * - Entry scopes are closed when entries expire, are invalidated, are evicted, + * are replaced, or when the cache's owning scope closes + * - Successful and failed lookup exits are both cached according to the + * configured TTL + * - Expired entries may remain counted by {@link size} until a cache operation + * observes and removes them + * - Once the owning scope closes, the cache is closed and lookup-style + * operations interrupt instead of acquiring new values + * + * @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 cached scoped resources need different lifetimes based on the 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-smol/packages/effect/src/ScopedRef.ts b/.repos/effect-smol/packages/effect/src/ScopedRef.ts new file mode 100644 index 00000000000..1b5ed400126 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/ScopedRef.ts @@ -0,0 +1,206 @@ +/** + * The `ScopedRef` module provides a mutable reference for values that are tied + * to scoped resources. Each value stored in a `ScopedRef` is acquired within its + * own `Scope`, and replacing the value safely releases the resources associated + * with the previous value. + * + * Use `ScopedRef` when an application needs to keep a current resource-backed + * value, such as a live client, connection, subscription, or cached handle, and + * later swap it for a newly acquired value without leaking the old resources. + * Reads are simple, while updates are synchronized and resource-safe. + * + * **Gotchas** + * + * - A `ScopedRef` must itself be created and used within a `Scope`; when that + * scope closes, the currently stored value is finalized. + * - Use {@link fromAcquire} or {@link set} for resourceful values so acquisition + * and finalization are tracked correctly. + * - Use {@link make} only for values that do not acquire resources. + * - Updating a `ScopedRef` waits for the replacement acquisition and old + * finalization to complete before returning. + * + * @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 resourcefully produces a + * 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 resourcefully 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 the specified resourcefully-created + * value, releasing any resources associated with the old value. + * + * **When to use** + * + * Use to replace the current value of an existing `ScopedRef` with a + * resourcefully acquired 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-smol/packages/effect/src/Semaphore.ts b/.repos/effect-smol/packages/effect/src/Semaphore.ts new file mode 100644 index 00000000000..1d1d3736e81 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Semaphore.ts @@ -0,0 +1,539 @@ +/** + * The `Semaphore` module provides counting semaphores for limiting concurrent + * access to shared resources. A semaphore owns a pool of permits; effects take + * permits before running protected work and return them when the work exits. + * + * **Mental model** + * + * - The permit count is the maximum amount of guarded work that can run at once + * - {@link withPermit} and {@link withPermits} acquire permits around one + * effect and release them on success, failure, or interruption + * - {@link take} and {@link release} expose the lower-level protocol when + * acquisition and release cannot be scoped to one effect + * - {@link resize} changes future availability while preserving permits that + * are already taken + * + * **Common tasks** + * + * - Create a semaphore: {@link make}, {@link makeUnsafe} + * - Guard one effect: {@link withPermit}, {@link withPermits} + * - Run only when permits are immediately available: + * {@link withPermitsIfAvailable} + * - Manage permits manually: {@link take}, {@link release}, {@link releaseAll} + * - Change capacity: {@link resize} + * + * **Gotchas** + * + * - Pending acquisitions wait until enough permits are available + * - {@link withPermitsIfAvailable} never waits; it returns `Option.none` when + * the requested permits are not available immediately + * - Manual {@link take} / {@link release} usage must keep permit counts + * balanced; prefer scoped helpers when possible + * + * @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 to manually acquire permits when a lower-level permit protocol needs + * 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 to manually return permits acquired with `take` when a lower-level + * permit protocol needs 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-smol/packages/effect/src/Sink.ts b/.repos/effect-smol/packages/effect/src/Sink.ts new file mode 100644 index 00000000000..7ddde01bdf4 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Sink.ts @@ -0,0 +1,2197 @@ +/** + * The `Sink` module provides composable consumers for `Stream` values. A + * `Sink` pulls input elements of type `In`, may require + * services `R`, may fail with `E`, and eventually produces a result `A` plus + * any leftover input `L` that was read but not consumed. + * + * **Mental model** + * + * - A sink is the terminal consumer used by `Stream.run` + * - Sinks can consume zero, one, many, or all input elements before finishing + * - Leftovers allow one sink to stop early without losing already-pulled input + * - Sink composition preserves typed errors and service requirements + * - Most sinks are built from `Channel` internally, but users compose them with + * the higher-level APIs in this module + * + * **Common tasks** + * + * - Create simple sinks: {@link succeed}, {@link fail}, {@link fromEffect} + * - Fold input: {@link fold} + * - Collect values: {@link collect} + * - Count or drain input: {@link count}, {@link drain} + * - Transform results: {@link map}, {@link mapEffect}, {@link as} + * - Adapt input before consumption: {@link mapInput}, {@link mapInputEffect} + * + * **Gotchas** + * + * - A sink can finish before the stream is exhausted; check leftover-aware + * combinators when composing parsers or protocol decoders + * - `In` is contravariant, so a sink that accepts broader input can be used + * where narrower input is expected + * - Resource and service requirements are tracked in the `R` type parameter + * + * @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 the effect needs to provide both the 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 utils + * @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 transforming a sink result itself is effectful, can fail, or needs + * 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 consuming all upstream input and only the final element is needed. + * + * **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 deciding whether an input matches requires an effect, can fail, or + * needs services. + * + * **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 to collect all upstream input elements into a single array when you need + * a sink result containing the complete input. + * + * @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 utils + * @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 utils + * @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-smol/packages/effect/src/Stdio.ts b/.repos/effect-smol/packages/effect/src/Stdio.ts new file mode 100644 index 00000000000..1a5be7c2f59 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Stdio.ts @@ -0,0 +1,163 @@ +/** + * Service contract for process standard input, output, error output, and + * command-line arguments. + * + * `Stdio` lets command-line 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`. + * + * **Mental model** + * + * Application code describes what it needs from standard I/O, and a runtime + * layer supplies the concrete streams. Platform packages provide real process + * implementations, while tests can use `Stdio.layerTest` to replace only the + * fields that matter for a scenario and keep the rest inert. + * + * **Common tasks** + * + * - Read command-line arguments from the service's `args` effect. + * - Write text or bytes by running values into the service's `stdout()` or + * `stderr()` sinks. + * - Consume `stdin` as a stream of `Uint8Array` chunks. + * - Build deterministic tests with `Stdio.layerTest`. + * + * **Gotchas** + * + * Standard I/O is a platform capability. Reads and writes can fail with + * `PlatformError`, so handle failures in the Effect error channel instead of + * assuming the process streams are always available. + * + * @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 an effect needs command-line arguments or standard I/O streams + * supplied by its 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 to assemble a concrete `Stdio` service when you already have + * implementations for command-line arguments, standard output, standard error, + * and standard input. + * + * **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-smol/packages/effect/src/Stream.ts b/.repos/effect-smol/packages/effect/src/Stream.ts new file mode 100644 index 00000000000..354f6944d66 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Stream.ts @@ -0,0 +1,11678 @@ +/** + * The `Stream` module describes effectful sequences that may emit many values + * over time. A `Stream` can produce zero or more `A` values, fail with + * an `E`, and require services from `R`; the Effect runtime handles + * backpressure, interruption, scopes, and finalizers while the stream is being + * consumed. + * + * Streams are useful for files, sockets, queues, subscriptions, paginated APIs, + * background jobs, and any workflow where values should be processed + * incrementally instead of loaded into memory all at once. + * + * **Mental model** + * + * - A stream is a lazy description; it does not run until consumed with + * {@link run}, {@link runCollect}, {@link runForEach}, or another `run*` + * function. + * - Pulling drives evaluation. Operators request values from upstream, and the + * runtime propagates demand instead of pushing unbounded data downstream. + * - Values are batched internally as chunks for throughput, while user-facing + * combinators still work with individual elements unless they mention chunks. + * - `A` is the element type, `E` is the failure type, and `R` is the required + * service context. + * - Composition mirrors `Effect`: use `pipe`, {@link map}, {@link flatMap}, + * error handling, resource operators, and service provisioning. + * + * **Common tasks** + * + * - Create streams from values, effects, and collections with {@link make}, + * {@link fromEffect}, {@link fromIterable}, and {@link fromQueue}. + * - Transform or select values with {@link map}, {@link mapEffect}, + * {@link flatMap}, {@link filter}, and {@link filterMap}. + * - Combine streams with {@link concat}, {@link merge}, {@link zip}, + * {@link race}, and {@link interleave}. + * - Control size and timing with {@link take}, {@link drop}, {@link debounce}, + * {@link throttle}, {@link grouped}, and {@link groupedWithin}. + * - Handle failures with {@link catchCause}, {@link catchIf}, + * {@link mapError}, {@link retry}, and {@link withExecutionPlan}. + * - Connect to other protocols with {@link fromReadableStream}, + * {@link toReadableStream}, {@link fromAsyncIterable}, {@link toQueue}, and + * {@link runIntoQueue}. + * - Consume streams with {@link runCollect}, {@link runForEach}, + * {@link runFold}, {@link runDrain}, or a {@link Sink.Sink}. + * + * **Quickstart** + * + * **Example** (Transforming and collecting values) + * + * ```ts + * import { Effect, Stream } from "effect" + * + * const program = Stream.make(1, 2, 3).pipe( + * Stream.map((n) => n * 2), + * Stream.runCollect + * ) + * + * Effect.runPromise(program).then(console.log) + * // [2, 4, 6] + * ``` + * + * **Gotchas** + * + * - A stream is not a collection. Constructors and operators build a + * description; effects run each time the stream is consumed. + * - {@link runCollect} stores every emitted value in memory. Prefer + * {@link runForEach}, {@link runFold}, or a streaming sink for large or + * infinite streams. + * - Operators such as {@link merge}, {@link race}, {@link broadcast}, and + * {@link share} introduce concurrency, so interruption and finalizer timing + * can matter. + * - Reusing the same stream value does not share execution by itself. Use + * {@link share}, {@link broadcast}, queues, or external state when multiple + * consumers must observe one running producer. + * + * **See also** + * + * - {@link Effect.Effect} for single-result effectful programs. + * - {@link Sink.Sink} for reusable stream consumers. + * - {@link Channel.Channel} for the lower-level primitive behind streams. + * - {@link Queue.Queue} and {@link PubSub.PubSub} for coordinating producers + * and consumers. + * + * @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 Type-Level + * @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 Type-Level + * @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 Type-Level + * @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 to emit an optional dependency as a stream element without requiring that + * dependency 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 can use the `Queue` with the apis from the `Queue` module to emit + * values to the stream or to signal the stream ending. + * + * 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. + * + * **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. + * + * **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()) + ) + 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.both(Schedule.recurs(2)) + * ) + * 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 to create the subscription and `Stream.take` or + * cancellation to control 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. + * + * **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. + * + * **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. + * + * **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** (FlatMapping 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. + * + * **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`. + * + * **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. + * + * **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. + * + * **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. + * + * **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. + * + * **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. + * + * **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 each stream element must be classified by an effectful `Filter` and + * both passing and failing mapped values need to be consumed 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 your error type is a tagged union with a readonly `_tag` + * field and you want to handle a specific error case. + * + * **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. 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`, and the recovery needs 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. 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 a stream may fail with defects and you want to silently suppress the + * 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`. + * + * **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`. + * + * **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`. + * + * **Details** + * + * The first element that satisfies the predicate is also 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. + * + * **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. + * + * **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 a stream is already ordered by the grouping key and you 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. + * + * **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. + * + * **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. + * + * **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. + * + * **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. + * + * **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-smol/packages/effect/src/String.ts b/.repos/effect-smol/packages/effect/src/String.ts new file mode 100644 index 00000000000..8ae29e978e8 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/String.ts @@ -0,0 +1,1486 @@ +/** + * Operations for working with TypeScript `string` values. Use this module to + * normalize text, inspect and extract characters, search with `Option` results, + * split or iterate lines, convert identifier casing, and pass string instances + * to generic APIs. + * + * **Mental model** + * + * Strings are plain JavaScript strings. Many operations mirror native string + * methods while giving them Effect-style names and data-last forms for `pipe`. + * Operations that may miss, such as {@link at}, {@link charCodeAt}, + * {@link codePointAt}, {@link indexOf}, {@link lastIndexOf}, {@link match}, and + * {@link search}, return `Option` instead of sentinel values such as `-1`, + * `undefined`, or `null`. + * + * **Common tasks** + * + * - Coerce or narrow input: {@link String}, {@link isString} + * - Build and compare strings: {@link empty}, {@link concat}, {@link Order}, + * {@link Equivalence}, {@link ReducerConcat} + * - Change text shape: {@link trim}, {@link trimStart}, {@link trimEnd}, + * {@link toUpperCase}, {@link toLowerCase}, {@link capitalize}, + * {@link uncapitalize} + * - Slice, split, and inspect: {@link slice}, {@link substring}, + * {@link takeLeft}, {@link takeRight}, {@link split}, {@link length} + * - Search and match: {@link includes}, {@link startsWith}, {@link endsWith}, + * {@link indexOf}, {@link lastIndexOf}, {@link match}, {@link matchAll}, + * {@link search} + * - Normalize identifiers and text blocks: {@link camelCase}, + * {@link pascalCase}, {@link snakeCase}, {@link kebabCase}, + * {@link constantCase}, {@link linesIterator}, {@link linesWithSeparators}, + * {@link stripMargin} + * + * **Gotchas** + * + * - {@link length} reports JavaScript string length in UTF-16 code units, not + * user-perceived characters. + * - {@link String} is the native JavaScript constructor. `String.String(value)` + * returns native string coercion results. + * - {@link split} always returns a non-empty array; splitting `""` with `""` + * returns `[""]`. + * - {@link replace} and {@link replaceAll} follow native JavaScript behavior. + * In particular, `replace` only replaces all matches when given a global + * regular expression. + * - Fixed converters such as {@link snakeToCamel} expect their named input + * shape. For mixed free-form input, prefer {@link camelCase}, + * {@link pascalCase}, {@link snakeCase}, {@link kebabCase}, or + * {@link noCase}. + * + * **Quickstart** + * + * **Example** (Normalizing and searching text) + * + * ```ts + * import { String } from "effect" + * + * const slug = String.kebabCase("User profile ID") + * console.log(slug) // "user-profile-id" + * + * const parts = String.split(slug, "-") + * console.log(parts) // ["user", "profile", "id"] + * + * const firstDash = String.indexOf("-")(slug) + * console.log(firstDash) // Option.some(4) + * ``` + * + * @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** (Using 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 concatenating + * @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 utils + * @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 to normalize mixed-case, snake_case, kebab-case, or spaced input into + * custom word-case output when you need 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 + const result = input + .replace(SPLIT_REGEXP[0], "$1\0$2") + .replace(SPLIT_REGEXP[1], "$1\0$2") + .replace(STRIP_REGEXP, "\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"). +const 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, index: number): string => { + const firstChar = input.charAt(0) + const lowerChars = input.substring(1).toLowerCase() + if (index > 0 && firstChar >= "0" && firstChar <= "9") { + return `_${firstChar}${lowerChars}` + } + return `${firstChar.toUpperCase()}${lowerChars}` +} + +/** + * 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, index) + +/** + * 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 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 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 concatenating + * @since 4.0.0 + */ +export const ReducerConcat: Reducer.Reducer = Reducer.make((a, b) => a + b, "") diff --git a/.repos/effect-smol/packages/effect/src/Struct.ts b/.repos/effect-smol/packages/effect/src/Struct.ts new file mode 100644 index 00000000000..e5530c1519d --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Struct.ts @@ -0,0 +1,1039 @@ +/** + * Utilities for creating, transforming, and comparing plain TypeScript objects + * (structs). Every function produces a new object — inputs are never mutated. + * + * ## Mental model + * + * - **Struct**: A plain JS object with a fixed set of known keys (e.g., + * `{ name: string; age: number }`). Not a generic key-value record. + * - **Dual API**: Most functions accept arguments in both data-first + * (`Struct.pick(obj, keys)`) and data-last (`pipe(obj, Struct.pick(keys))`) + * style. + * - **Immutability**: All operations return a new object; the original is + * never modified. + * - **Lambda**: A type-level function interface (`~lambda.in` / `~lambda.out`) + * used by {@link map}, {@link mapPick}, and {@link mapOmit} so the compiler + * can track how value types change. + * - **Evolver pattern**: {@link evolve}, {@link evolveKeys}, and + * {@link evolveEntries} let you selectively transform values, keys, or both + * while leaving untouched properties unchanged. + * + * ## Common tasks + * + * - Access a property in a pipeline → {@link get} + * - List string keys with proper types → {@link keys} + * - Subset / remove properties → {@link pick}, {@link omit} + * - Merge two structs (second wins) → {@link assign} + * - Rename keys → {@link renameKeys} + * - Transform selected values → {@link evolve} + * - Transform selected keys → {@link evolveKeys} + * - Transform both keys and values → {@link evolveEntries} + * - Map all values with a typed lambda → {@link map}, {@link mapPick}, + * {@link mapOmit} + * - Compare structs → {@link makeEquivalence}, {@link makeOrder} + * - Combine / reduce structs → {@link makeCombiner}, {@link makeReducer} + * - Flatten intersection types → {@link Simplify} + * - Strip `readonly` modifiers → {@link Mutable} + * + * ## Gotchas + * + * - {@link keys} only returns `string` keys; symbol keys are excluded. + * - {@link pick} and {@link omit} iterate with `for...in`, which includes + * inherited enumerable properties but excludes non-enumerable ones. + * - {@link assign} spreads with `...`; property order follows standard + * JS spread rules. + * - {@link map}, {@link mapPick}, {@link mapOmit} require a {@link Lambda} + * value created with {@link lambda}; a plain function won't type-check. + * + * ## Quickstart + * + * **Example** (Picking, renaming, and evolving struct properties) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * const user = { firstName: "Alice", lastName: "Smith", age: 30, admin: false } + * + * const result = pipe( + * user, + * Struct.pick(["firstName", "age"]), + * Struct.evolve({ age: (n) => n + 1 }), + * Struct.renameKeys({ firstName: "name" }) + * ) + * + * console.log(result) // { name: "Alice", age: 31 } + * ``` + * + * ## See also + * + * - {@link Equivalence} – building equivalence relations for structs + * - `Order` – ordering structs by their fields + * - {@link Combiner} – combining two values of the same type + * - {@link Reducer} – combining with an initial value + * + * @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 Type-Level Programming + * @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 Type-Level Programming + * @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 Type-Level Programming + * @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 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 use instead of `Object.keys` when you want the return type narrowed to the + * known keys of the struct. + * + * **Gotchas** + * + * Symbol keys are excluded; only string keys are returned. + * + * **Example** (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 utils + * @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 to compare structs property-by-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 Equivalence + * @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 to sort or compare structs by multiple fields with lexicographic + * priority. + * + * **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 you use this interface 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 in source) { + 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 structs of the same shape, 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 to fold a collection of structs into a single summary struct. + * + * **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-smol/packages/effect/src/SubscriptionRef.ts b/.repos/effect-smol/packages/effect/src/SubscriptionRef.ts new file mode 100644 index 00000000000..b903c0fa7ae --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/SubscriptionRef.ts @@ -0,0 +1,1063 @@ +/** + * The `SubscriptionRef` module combines a fiber-safe mutable reference with a + * replaying stream of state changes. A `SubscriptionRef` stores the latest + * value, serializes updates, and publishes each committed value so subscribers + * can observe state as it evolves. + * + * **Mental model** + * + * - {@link make} creates the reference and immediately publishes the initial + * value. + * - {@link get} reads the latest value without subscribing. + * - {@link set}, {@link update}, and {@link modify} change the value under the + * reference semaphore and publish the new value. + * - {@link changes} returns a stream that first emits the current value and + * then emits future published values. + * - The `Some` variants leave the value unchanged and publish nothing when + * their `Option` result is empty. + * + * **Common tasks** + * + * - Create shared state with {@link make}. + * - Read once with {@link get} or observe over time with {@link changes}. + * - Replace state with {@link set}, {@link setAndGet}, or {@link getAndSet}. + * - Transform state with {@link update}, {@link updateAndGet}, + * {@link getAndUpdate}, or their effectful variants. + * - Compute a separate result while updating with {@link modify} or + * {@link modifyEffect}. + * + * **Example** (Reading the current value through changes) + * + * ```ts + * import { Effect, Stream, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(0) + * + * yield* SubscriptionRef.update(ref, (n) => n + 1) + * + * const latest = yield* SubscriptionRef.changes(ref).pipe( + * Stream.take(1), + * Stream.runCollect + * ) + * + * return latest + * }) + * ``` + * + * **Gotchas** + * + * - Every successful set or non-empty update is published, even when the new + * value is equal to the old one. + * - New subscribers receive the current value from the replay buffer before + * future updates. + * - Unsafe helpers bypass the semaphore and should only be used when the caller + * already controls access. + * + * @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 shared mutable state 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. + * + * **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 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 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 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 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-smol/packages/effect/src/Symbol.ts b/.repos/effect-smol/packages/effect/src/Symbol.ts new file mode 100644 index 00000000000..93c1a964bbd --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Symbol.ts @@ -0,0 +1,48 @@ +/** + * 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. + * + * **Mental model** + * + * - {@link isSymbol} checks `typeof value === "symbol"` + * - Symbols created with `globalThis.Symbol()` or `globalThis.Symbol.for(...)` + * satisfy the predicate + * - Boxed symbols such as `Object(globalThis.Symbol())` are objects, so they do + * not satisfy the predicate + * + * **Example** (Narrowing unknown input) + * + * ```ts + * import { Symbol } from "effect" + * + * const describe = (value: unknown) => + * Symbol.isSymbol(value) ? value.description : "not a symbol" + * ``` + * + * @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-smol/packages/effect/src/SynchronizedRef.ts b/.repos/effect-smol/packages/effect/src/SynchronizedRef.ts new file mode 100644 index 00000000000..0040ac2ff6a --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/SynchronizedRef.ts @@ -0,0 +1,672 @@ +/** + * The `SynchronizedRef` module provides mutable references whose updates are + * serialized, including updates that run effects before deciding the next + * value. A `SynchronizedRef` behaves like a `Ref` for reading and basic + * updates, but uses an internal semaphore so concurrent modifications observe a + * consistent current value and apply one at a time. + * + * **When to use** + * + * Use to coordinate shared state that may be updated by many fibers + * - Running effectful state transitions that must not overlap + * - Computing both a return value and a new stored value atomically + * - Applying partial updates with `Option`, where `None` leaves the value + * unchanged + * + * **Gotchas** + * + * - Effectful update functions run while the semaphore is held, so long-running + * effects delay other updates to the same ref + * - Failed effectful updates do not replace the stored value + * - `getUnsafe` and `makeUnsafe` bypass the `Effect` API and should be reserved + * for low-level or carefully controlled code + * + * @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 synchronous construction is required 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 synchronized reference 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 utils + * @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 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 utils + * @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 an effectful state transition must 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 utils + * @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 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 utils + * @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 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 utils + * @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 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 utils + * @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 + * 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 utils + * @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 the stored value. + * + * @see {@link modify} for always storing a new value + * @see {@link updateSome} for optional updates without a separate return value + * + * @category utils + * @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 value. + * + * @see {@link modifySome} for the pure variant + * @see {@link updateSomeEffect} for effectful optional updates without a separate return value + * + * @category utils + * @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 utils + * @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 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 utils + * @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 utils + * @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 utils + * @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 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 utils + * @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 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 utils + * @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 update without returning a value. + * + * @see {@link update} for always applying a pure update + * @see {@link updateSomeAndGet} for returning the resulting current value + * + * @category utils + * @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 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 utils + * @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 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 utils + * @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 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 utils + * @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-smol/packages/effect/src/Take.ts b/.repos/effect-smol/packages/effect/src/Take.ts new file mode 100644 index 00000000000..1d284ab7695 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Take.ts @@ -0,0 +1,70 @@ +/** + * 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. + * + * **Mental model** + * + * - A value batch represents elements that were pulled successfully + * - A failed `Exit` represents an ordinary pull failure + * - A successful `Exit` represents normal completion and carries `Done` + * - {@link toPull} interprets the stored result as a `Pull.Pull` step + * + * **Common tasks** + * + * - Store or transfer one pull result as {@link Take} + * - Turn a stored result back into a pull step with {@link toPull} + * + * **Gotchas** + * + * - Value batches are `NonEmptyReadonlyArray` values; empty arrays are not + * valid `Take` values + * - Successful `Exit` values do not emit elements; they signal completion + * - `Take` is a representation, not a queue or stream by itself + * + * **See also** + * + * - {@link Pull.Pull} for the pull-step effect interpreted by {@link toPull} + * - {@link Exit.Exit} for the success and failure outcomes stored by `Take` + * + * @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-smol/packages/effect/src/Terminal.ts b/.repos/effect-smol/packages/effect/src/Terminal.ts new file mode 100644 index 00000000000..6615eacb1c2 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Terminal.ts @@ -0,0 +1,209 @@ +/** + * The `Terminal` module defines Effect's service for interactive terminal + * capabilities. Programs can query dimensions, read a line of input, subscribe + * to low-level key events, and display text without depending directly on Node, + * the browser, or a test-specific console implementation. + * + * **Mental model** + * + * `Terminal` sits above raw standard I/O. `Stdio` exposes process streams, + * while `Terminal` exposes already interpreted terminal operations as an Effect + * service. Application code depends on the {@link Terminal} service; platform + * runtimes and tests provide the concrete implementation, often by constructing + * one with {@link make}. + * + * **Common tasks** + * + * - Read prompt answers with `readLine` and handle user cancellation through + * {@link QuitError}. + * - Build interactive prompts from `readInput` by consuming {@link UserInput} + * values with parsed key metadata and optional raw character input. + * - Write terminal output through `display` while keeping platform failures in + * the Effect error channel. + * + * **Gotchas** + * + * - `readLine` can fail with {@link QuitError} when the user requests to quit, + * commonly via `Ctrl+C`. + * - `readInput` requires a `Scope` because the returned dequeue represents a + * live input subscription. + * + * @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 tags + * @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-smol/packages/effect/src/Tracer.ts b/.repos/effect-smol/packages/effect/src/Tracer.ts new file mode 100644 index 00000000000..cc3396cfdd2 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Tracer.ts @@ -0,0 +1,713 @@ +/** + * The `Tracer` module defines the low-level tracing model used by Effect to + * describe and propagate spans. A span records the lifetime of an operation, + * including its name, parent, attributes, links, annotations, sampling decision, + * kind, and completion status. + * + * **Mental model** + * + * - `Tracer` is the backend interface responsible for creating spans + * - `Span` values represent Effect-managed operations with mutable lifecycle + * hooks for ending spans and adding attributes, events, or links + * - `ExternalSpan` represents trace context imported from another tracing + * system so Effect spans can be parented by or linked to external work + * - `ParentSpan`, `Tracer`, and related context references control propagation, + * sampling, and trace-level filtering through the Effect context + * + * **Common tasks** + * + * - Implement a custom tracing backend with {@link make} + * - Provide or inspect parent span context with {@link ParentSpan} + * - Convert external trace identifiers into Effect span values with + * {@link externalSpan} + * - Configure span metadata with {@link SpanOptions}, {@link SpanKind}, and + * {@link SpanLink} + * - Disable propagation or adjust trace filtering with + * {@link DisablePropagation}, {@link CurrentTraceLevel}, and + * {@link MinimumTraceLevel} + * + * **Gotchas** + * + * - This module exposes the tracing data model and backend hooks; most + * application code should create spans through higher-level Effect APIs such + * as `Effect.withSpan` + * - `ExternalSpan` only carries identity and metadata from another system; it + * does not have lifecycle methods like `Span` + * - Propagation and sampling are context-dependent, so parent selection can be + * affected by disabled propagation, root span options, and trace-level + * thresholds + * + * @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 integrating lower-level tracing code that needs the raw context key + * for parent span lookup. + * + * **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 tags + * @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 tags + * @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 models + * @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 models + * @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 models + * @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 integrating lower-level tracing code that needs the raw context key + * for active tracer lookup. + * + * @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-smol/packages/effect/src/Trie.ts b/.repos/effect-smol/packages/effect/src/Trie.ts new file mode 100644 index 00000000000..898b8a94047 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Trie.ts @@ -0,0 +1,991 @@ +/** + * Immutable string-keyed maps optimized for prefix lookup. + * + * A `Trie` stores values under `string` keys, similar to a `HashMap` + * whose key type is fixed to `string`. That restriction lets the data + * structure organize keys by their shared prefixes, which is useful for + * autocomplete, route tables, dictionaries, command lookup, and any workflow + * that needs to ask "which entries start with this string?". + * + * **Mental model** + * + * - Exact-key lookup uses {@link get}, {@link has}, or {@link getUnsafe}. + * - Prefix lookup uses {@link keysWithPrefix}, {@link valuesWithPrefix}, + * {@link entriesWithPrefix}, {@link toEntriesWithPrefix}, or + * {@link longestPrefixOf}. + * - Iteration yields entries in key order, not insertion order. + * - Lookup work is proportional to the key or prefix length rather than the + * total number of entries. + * + * **Common tasks** + * + * - Create tries with {@link empty}, {@link make}, or {@link fromIterable}. + * - Read all keys, values, or entries with {@link keys}, {@link values}, + * {@link entries}, and {@link toEntries}. + * - Transform values with {@link map}, {@link filter}, {@link filterMap}, + * {@link compact}, {@link forEach}, and {@link reduce}. + * + * **Gotchas** + * + * - Keys must be strings. Use `HashMap` when keys need structural equality or + * are not naturally represented as strings. + * - {@link get} returns an `Option`; {@link getUnsafe} throws when the key is + * absent. + * - Sorted iteration is convenient for presentation, but it also means + * insertion order is not preserved. + * + * **Example** (Finding entries by prefix) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const commands = Trie.make( + * ["commit", "Record changes"], + * ["checkout", "Switch branches"], + * ["clone", "Copy a repository"] + * ) + * + * const matches = Trie.toEntriesWithPrefix(commands, "ch") + * assert.deepStrictEqual(matches, [["checkout", "Switch branches"]]) + * ``` + * + * @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. + * + * **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-smol/packages/effect/src/Tuple.ts b/.repos/effect-smol/packages/effect/src/Tuple.ts new file mode 100644 index 00000000000..cf4d90a2e99 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Tuple.ts @@ -0,0 +1,791 @@ +/** + * Utilities for creating, accessing, transforming, and comparing fixed-length + * arrays (tuples). Every function produces a new tuple — inputs are never + * mutated. + * + * ## Mental model + * + * - **Tuple**: A fixed-length readonly array where each position can have a + * different type (e.g., `readonly [string, number, boolean]`). + * - **Index-based access**: Elements are accessed by numeric index, and the + * type system tracks the type at each position. + * - **Dual API**: Most functions accept arguments in both data-first + * (`Tuple.get(t, 0)`) and data-last (`pipe(t, Tuple.get(0))`) style. + * - **Immutability**: All operations return a new tuple; the original is + * never modified. + * - **Lambda**: A type-level function interface (from `Struct`) used by + * {@link map}, {@link mapPick}, and {@link mapOmit} so the compiler can + * track how element types change. + * + * ## Common tasks + * + * - Create a tuple → {@link make} + * - Access an element by index → {@link get} + * - Select / remove elements by index → {@link pick}, {@link omit} + * - Append elements → {@link appendElement}, {@link appendElements} + * - Transform selected elements → {@link evolve} + * - Swap element positions → {@link renameIndices} + * - Map all elements with a typed lambda → {@link map}, {@link mapPick}, + * {@link mapOmit} + * - Compare tuples → {@link makeEquivalence}, {@link makeOrder} + * - Combine / reduce tuples → {@link makeCombiner}, {@link makeReducer} + * - Check tuple length at runtime → `isTupleOf`, `isTupleOfAtLeast` + * + * ## Gotchas + * + * - {@link pick} and {@link omit} use numeric indices, not string keys. + * - {@link renameIndices} takes an array of stringified source indices + * (e.g., `["2", "1", "0"]`), not arbitrary names. + * - {@link map}, {@link mapPick}, {@link mapOmit} require a Lambda value + * created with `Struct.lambda`; a plain function won't type-check. + * - `isTupleOf` and `isTupleOfAtLeast` only check length, not + * element types. + * + * ## Quickstart + * + * **Example** (Creating and transforming a tuple) + * + * ```ts + * import { pipe, Tuple } from "effect" + * + * const point = Tuple.make(10, 20, "red") + * + * const result = pipe( + * point, + * Tuple.evolve([ + * (x) => x * 2, + * (y) => y * 2 + * ]) + * ) + * + * console.log(result) // [20, 40, "red"] + * ``` + * + * ## See also + * + * - `Struct` – similar utilities for objects with named keys + * - {@link Array} – operations on variable-length arrays + * + * @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 use this instead of `[a, b, c] as const` when you want a properly typed tuple + * without a 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 you use this in a pipeline when you need to extract a single element. + * + * **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 utils + * @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 utils + * @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 to add one element to the end of a tuple while preserving tuple types. + * + * **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 Concatenating + * @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 Concatenating + * @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 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 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 Equivalence + * @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 to sort or compare tuples lexicographically by element position. + * + * **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 against unexpected array lengths 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 expected number of + * elements. + * + * **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 tuples of the same shape, 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 to fold a collection of tuples into a single 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-smol/packages/effect/src/TxChunk.ts b/.repos/effect-smol/packages/effect/src/TxChunk.ts new file mode 100644 index 00000000000..c8aa087c0f4 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxChunk.ts @@ -0,0 +1,854 @@ +/** + * The `TxChunk` module provides a transactional collection backed by an + * immutable {@link Chunk}. A `TxChunk` stores the current chunk in a + * {@link TxRef}, so reads and updates are tracked by Effect transactions and + * committed atomically. + * + * **Mental model** + * + * - A `TxChunk` is mutable at the reference level, but every stored value is a + * persistent `Chunk` + * - Single operations such as {@link append}, {@link drop}, or {@link get} run + * as transactions on their own + * - Wrap several operations in `Effect.tx` when they must observe one + * consistent snapshot and commit or retry together + * - If a transaction reads a `TxChunk` and another transaction changes it + * before commit, the transaction retries instead of publishing a stale write + * + * **Common tasks** + * + * - Create collections: {@link empty}, {@link make}, {@link fromIterable} + * - Replace or transform contents: {@link set}, {@link update}, {@link modify} + * - Add values: {@link append}, {@link prepend}, {@link appendAll}, + * {@link prependAll} + * - Keep or remove ranges: {@link take}, {@link drop}, {@link slice}, + * {@link filter} + * - Inspect contents: {@link get}, {@link size}, {@link isEmpty}, + * {@link isNonEmpty} + * + * **Gotchas** + * + * - `get` returns the current immutable `Chunk`, not a live mutable view + * - Operations such as {@link append}, {@link drop}, and {@link filter} update + * the stored chunk; they do not return a new `TxChunk` + * - Use `TxQueue` instead when producers should wait, drop, or slide based on + * queue capacity + * + * **See also** + * + * - {@link TxRef} for the lower-level transactional reference used internally + * - {@link Chunk} for immutable chunk operations + * - `TxQueue` for transactional producer/consumer queues + * + * @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-smol/packages/effect/src/TxDeferred.ts b/.repos/effect-smol/packages/effect/src/TxDeferred.ts new file mode 100644 index 00000000000..89ea4b6ef0d --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxDeferred.ts @@ -0,0 +1,337 @@ +/** + * 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. + * + * **Mental model** + * + * `TxDeferred` is the transaction-aware counterpart to a regular deferred + * value. Completion is single-assignment, reads participate in transaction + * retry semantics, and all observers see the same committed result once the + * deferred has been completed. + * + * **Common tasks** + * + * Create an empty deferred with {@link make}, wait for completion with + * `await`, inspect the current state with {@link poll}, and complete it with + * {@link done}, {@link succeed}, or {@link fail}. + * + * **Gotchas** + * + * Only the first completion wins. Later completion attempts return `false` + * instead of replacing the stored result. + * + * @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-smol/packages/effect/src/TxHashMap.ts b/.repos/effect-smol/packages/effect/src/TxHashMap.ts new file mode 100644 index 00000000000..98f35b93cd9 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxHashMap.ts @@ -0,0 +1,2182 @@ +/** + * The `TxHashMap` module provides a transactional hash map for storing and + * updating key-value pairs inside Effect transactions. It is useful when + * multiple fibers need to coordinate shared map state and each read-modify-write + * sequence must be committed atomically. + * + * A `TxHashMap` has the familiar shape of a `HashMap`, but every + * operation returns an `Effect` and participates in transaction semantics + * through `TxRef`. Use it for concurrent registries, caches, counters, indexes, + * and other mutable maps whose updates should compose safely with other + * transactional references. + * + * **Common tasks** + * + * - Create maps with {@link empty}, {@link fromIterable}, or {@link make} + * - Read entries with {@link get}, {@link has}, {@link keys}, {@link values}, and {@link entries} + * - Update entries with {@link set}, {@link modify}, {@link modifyAt}, and {@link remove} + * - Inspect aggregate state with {@link size}, {@link isEmpty}, and {@link reduce} + * + * **Gotchas** + * + * - Operations are effectful; run them in `Effect.gen` and wrap multi-step + * transactions with `Effect.tx` when the whole sequence must commit together. + * - Reads that may be absent return `Option`, so handle both `Some` and `None` + * instead of assuming a key exists. + * + * @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 type-level + * @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 type-level + * @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 type-level + * @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-smol/packages/effect/src/TxHashSet.ts b/.repos/effect-smol/packages/effect/src/TxHashSet.ts new file mode 100644 index 00000000000..9fe67d897a7 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxHashSet.ts @@ -0,0 +1,917 @@ +/** + * The `TxHashSet` module provides a transactional hash set for storing unique + * values inside Effect transactions. A `TxHashSet` wraps a `HashSet` in a + * transactional reference, so reads and writes can be composed with other + * transactional operations and committed atomically. + * + * **Common tasks** + * + * - Create transactional sets with {@link empty}, {@link make}, or {@link fromIterable} + * - Mutate an existing set with {@link add}, {@link remove}, and {@link clear} + * - Query membership and size with {@link has}, {@link size}, and {@link isEmpty} + * - Derive new sets with {@link map}, {@link filter}, {@link union}, {@link intersection}, and {@link difference} + * - Fold or collect values with {@link reduce} and {@link toHashSet} + * + * **Gotchas** + * + * - Mutation operations update the same transactional set; transform operations + * return a new `TxHashSet` + * - Operations are `Effect` values and must be yielded, piped, or run to take effect + * - Use `Effect.tx` when several operations must observe and commit one atomic transaction + * + * @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 type-level + * @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-smol/packages/effect/src/TxPriorityQueue.ts b/.repos/effect-smol/packages/effect/src/TxPriorityQueue.ts new file mode 100644 index 00000000000..f607e92c29b --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxPriorityQueue.ts @@ -0,0 +1,587 @@ +/** + * The `TxPriorityQueue` module provides a mutable priority queue whose state is + * stored in a transactional reference. 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. + * + * A `TxPriorityQueue` is useful when multiple fibers coordinate through a + * shared queue and the queue operation needs to compose with other + * transactional state changes. Every operation returns an `Effect`; group + * several queue operations with `Effect.tx` when the whole sequence must commit + * or retry as one transaction. + * + * **Common tasks** + * + * - Create queues with {@link empty}, {@link fromIterable}, or {@link make} + * - Insert values with {@link offer} and {@link offerAll} + * - Read priority order with {@link peek}, {@link peekOption}, and + * {@link toArray} + * - Remove values with {@link take}, {@link takeOption}, {@link takeAll}, and + * {@link takeUpTo} + * - Keep or remove subsets with {@link retainIf} and {@link removeIf} + * + * **Gotchas** + * + * - `take` and `peek` retry when the queue is empty; use `takeOption` or + * `peekOption` when empty queues should be represented as `Option.none`. + * - `Order.Number` is ascending, so lower numbers are dequeued first. Provide a + * reversed order when larger values should have higher priority. + * - The queue preserves all values with equal priority; equal values are not + * merged or deduplicated. + * + * @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-smol/packages/effect/src/TxPubSub.ts b/.repos/effect-smol/packages/effect/src/TxPubSub.ts new file mode 100644 index 00000000000..25b25f5a2dc --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxPubSub.ts @@ -0,0 +1,722 @@ +/** + * The `TxPubSub` module provides a transactional publish/subscribe hub for + * broadcasting values to scoped subscribers. Each subscriber owns a `TxQueue`, + * so every value is offered independently to the queues registered at the time + * of publication. + * + * **Mental model** + * + * A `TxPubSub` is a registry of subscriber queues plus a shutdown flag. + * Publishing reads the current subscribers and offers the value to each queue + * in one transactional operation. Subscribers only receive values published + * after they subscribe, and `subscribe` removes the queue when its scope + * closes. + * + * **Common tasks** + * + * - Use `bounded` when slow subscribers should apply backpressure to + * publishers. + * - Use `dropping` when new messages may be skipped for full subscribers. + * - Use `sliding` when full subscribers should keep the newest messages. + * - Use `unbounded` when subscriber queues should grow without backpressure. + * + * **Example** (Broadcasting to a subscriber) + * + * ```ts + * import { Effect, TxPubSub, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * + * return yield* Effect.scoped( + * Effect.gen(function*() { + * const subscriber = yield* TxPubSub.subscribe(hub) + * yield* TxPubSub.publish(hub, "updated") + * return yield* TxQueue.take(subscriber) + * }) + * ) + * }) + * ``` + * + * **Gotchas** + * + * - `size` reports the maximum pending messages in any subscriber queue, not + * the total number of queued copies. + * - `shutdown` stops future publishes and shuts down subscriber queues that are + * registered at shutdown time. + * + * @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-smol/packages/effect/src/TxQueue.ts b/.repos/effect-smol/packages/effect/src/TxQueue.ts new file mode 100644 index 00000000000..ac35effa154 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxQueue.ts @@ -0,0 +1,1528 @@ +/** + * The `TxQueue` module provides queues whose state changes participate in + * Effect transactions. A `TxQueue` stores values of type `A`, exposes + * write-only {@link TxEnqueue} and read-only {@link TxDequeue} handles, and can + * complete or fail with causes observed by consumers. + * + * **Mental model** + * + * - Queue contents and lifecycle state are transactional, so a transaction can + * offer, take, inspect, and update other transactional values atomically + * - {@link take}, {@link takeAll}, {@link takeN}, {@link takeBetween}, and + * {@link peek} retry while the queue has no required value available + * - {@link bounded} queues retry producers when full; {@link dropping} + * rejects new values; {@link sliding} removes old values; {@link unbounded} + * always accepts while open + * - {@link interrupt}, {@link fail}, {@link failCause}, {@link end}, and + * {@link shutdown} move the queue toward completion and affect later offers + * and takes + * + * **Common tasks** + * + * - Create queues: {@link bounded}, {@link dropping}, {@link sliding}, + * {@link unbounded} + * - Produce values: {@link offer}, {@link offerAll} + * - Consume values: {@link take}, {@link poll}, {@link peek}, {@link takeN}, + * {@link takeBetween}, {@link takeAll} + * - Inspect state: {@link size}, {@link isEmpty}, {@link isFull}, + * {@link isOpen}, {@link isClosing}, {@link isDone} + * - Finish queues: {@link end}, {@link fail}, {@link failCause}, + * {@link interrupt}, {@link shutdown}, {@link awaitCompletion} + * + * **Gotchas** + * + * - {@link take} and {@link peek} are blocking in transactional terms: they + * use `Effect.txRetry` until an item is offered or the queue reaches a + * terminal state. Use {@link poll} when absence should be immediate. + * - {@link offer} returns `false` for closing or done queues, and for full + * {@link dropping} queues + * - Closing queues keep serving buffered values; done queues fail blocking + * consumers with the stored cause, while {@link poll} returns `Option.none` + * - `TxQueue` is for coordinating transactional state. Use `Queue` for general + * fiber communication outside an atomic transaction. + * + * **See also** + * + * - {@link TxEnqueue} for write-only queue handles + * - {@link TxDequeue} for read-only queue handles + * - {@link TxChunk} and {@link TxRef} for the transactional storage used by + * this module + * + * @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-smol/packages/effect/src/TxReentrantLock.ts b/.repos/effect-smol/packages/effect/src/TxReentrantLock.ts new file mode 100644 index 00000000000..726adb4cf52 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxReentrantLock.ts @@ -0,0 +1,686 @@ +/** + * The `TxReentrantLock` module provides a transactional read/write lock whose + * ownership is tracked per fiber. Multiple fibers may hold read locks at the + * same time, while a write lock gives one fiber exclusive access. + * + * **Mental model** + * + * The lock stores reader counts and an optional writer count in transactional + * state. A fiber can reacquire locks it already owns, so nested read or write + * sections are safe as long as each acquisition is matched by a release. A + * write acquisition waits when another fiber owns a read or write lock, and a + * read acquisition waits when another fiber owns the write lock. + * + * **Common tasks** + * + * - Use `withReadLock` to run an effect that may share access with other + * readers. + * - Use `withWriteLock` or `withLock` to run an effect with exclusive access. + * - Use `readLock` or `writeLock` when lock ownership should be tied to an + * existing scope. + * + * **Example** (Protecting a read/write workflow) + * + * ```ts + * import { Effect, Ref, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const state = yield* Ref.make(0) + * + * yield* TxReentrantLock.withWriteLock(lock, Ref.update(state, (n) => n + 1)) + * return yield* TxReentrantLock.withReadLock(lock, Ref.get(state)) + * }) + * ``` + * + * **Gotchas** + * + * - Manual acquisitions are counted; release the same number of times or use + * the scoped and `with*` helpers. + * - Releasing a lock from a fiber that does not own it leaves the lock + * unchanged and returns `0`. + * + * @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 as the short alias for {@link withWriteLock}. + * + * **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-smol/packages/effect/src/TxRef.ts b/.repos/effect-smol/packages/effect/src/TxRef.ts new file mode 100644 index 00000000000..f11578f2de9 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxRef.ts @@ -0,0 +1,332 @@ +/** + * The `TxRef` module provides 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. + * + * **Mental model** + * + * - {@link make} creates a reference whose value can participate in + * optimistic transactions. + * - {@link get}, {@link set}, {@link update}, and {@link modify} read and + * write the transaction journal when a transaction is active. + * - At commit time, the transaction checks whether any accessed reference was + * changed by another transaction. If so, it retries with a fresh journal. + * - `Effect.txRetry` suspends the transaction until one of the `TxRef` values + * read by the transaction changes. + * + * **Common tasks** + * + * - Create transactional state with {@link make}. + * - Read the current value with {@link get}. + * - Replace or transform the value with {@link set} and {@link update}. + * - Return a derived result while writing a new value with {@link modify}. + * - Wrap related reads and writes in one `Effect.tx` boundary so they commit or + * roll back as a unit. + * + * **Example** (Committing multiple updates atomically) + * + * ```ts + * import { Effect, TxRef } from "effect" + * + * const transfer = Effect.gen(function*() { + * const checking = yield* TxRef.make(100) + * const savings = yield* TxRef.make(0) + * + * yield* Effect.tx(Effect.gen(function*() { + * const balance = yield* TxRef.get(checking) + * if (balance < 30) { + * return yield* Effect.fail("insufficient funds") + * } + * yield* TxRef.set(checking, balance - 30) + * yield* TxRef.update(savings, (amount) => amount + 30) + * })) + * + * return { + * checking: yield* TxRef.get(checking), + * savings: yield* TxRef.get(savings) + * } + * }) + * ``` + * + * **Gotchas** + * + * - Group related operations in the same `Effect.tx` call; separate + * transactions can observe and commit intermediate states. + * - A transaction body can run more than once after a conflict or + * `Effect.txRetry`, so keep externally visible effects outside the + * transaction body or make them idempotent. + * - If a transaction fails, its journal is discarded and other fibers continue + * to see the last committed values. + * + * @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 transactional reference 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 transactional reference 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 transactional reference 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 transactional reference 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 transactional reference. + * + * **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 transactional reference. + * + * **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-smol/packages/effect/src/TxSemaphore.ts b/.repos/effect-smol/packages/effect/src/TxSemaphore.ts new file mode 100644 index 00000000000..5f93a8660b0 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxSemaphore.ts @@ -0,0 +1,735 @@ +/** + * Coordinate access to limited resources with a semaphore whose permit count + * participates in Effect transactions. A `TxSemaphore` keeps a fixed capacity + * and records permit changes in transactional state, so acquiring or releasing + * permits can commit atomically with related `TxRef` updates. + * + * **Mental model** + * + * - {@link make} creates a semaphore with a fixed number of permits. + * - {@link acquire} and {@link acquireN} wait by retrying the transaction when + * there are not enough permits available. + * - {@link tryAcquire} and {@link tryAcquireN} update the permit count only + * when enough permits are already available. + * - {@link release} and {@link releaseN} return permits, and the total cannot + * exceed the original capacity. + * - {@link withPermit} and {@link withPermits} bracket a supplied effect, + * while {@link withPermitScoped} holds one permit until the current scope + * closes. + * + * **Common tasks** + * + * - Reserve capacity before committing related transactional state changes. + * - Protect a pool, queue, or rate-limited section with permit accounting that + * composes with `TxRef`. + * - Inspect the current number of permits with {@link available} and the fixed + * total with {@link capacity}. + * + * **Gotchas** + * + * - Permit operations enter `Effect.tx`; group related transactional + * work inside the same boundary when it must commit together. + * - Requesting more permits than the semaphore capacity with + * {@link acquireN} can wait forever because the capacity never grows. + * - Creating a semaphore with negative permits, acquiring a non-positive number + * of permits, or releasing a non-positive number of permits defects. + * - Extra releases are capped at the fixed capacity. + * + * @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-smol/packages/effect/src/TxSubscriptionRef.ts b/.repos/effect-smol/packages/effect/src/TxSubscriptionRef.ts new file mode 100644 index 00000000000..b37a5b166d8 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/TxSubscriptionRef.ts @@ -0,0 +1,540 @@ +/** + * The `TxSubscriptionRef` module provides a transactional reference that can + * also be observed as a stream of committed values. It combines the read and + * update behavior of a `TxRef.TxRef` with a subscription channel, so each + * subscriber first receives the current value and then receives every value + * published by later updates. + * + * **Mental model** + * + * - The current value is stored transactionally and can be read with + * {@link get} + * - Mutations such as {@link set}, {@link update}, and {@link modify} commit a + * new value and publish that value to active subscribers + * - {@link changes} creates a scoped transactional queue for one subscriber + * - {@link changesStream} exposes the same change feed as a `Stream.Stream` + * - Subscriptions are per subscriber; each subscriber gets its own queue of + * committed values + * + * **Common tasks** + * + * - Create observable transactional state with {@link make} + * - Read or replace the current value with {@link get} and {@link set} + * - Derive new values atomically with {@link update} or {@link modify} + * - Consume changes from transactional code with {@link changes} + * - Consume changes from stream pipelines with {@link changesStream} + * + * **Gotchas** + * + * - A subscription starts with the value current at subscription time, not just + * future updates + * - The queue returned by {@link changes} is scoped; leaving the scope removes + * the subscriber + * - Updates are published even when the new value is equal to the previous + * value + * - Subscriber queues are unbounded, so long-lived slow subscribers can retain + * pending values until they catch up or their scope closes + * + * @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 transactional state that also 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 transactional 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 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 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 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 the value while returning the previous value. + * + * **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 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 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 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 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-smol/packages/effect/src/Types.ts b/.repos/effect-smol/packages/effect/src/Types.ts new file mode 100644 index 00000000000..6f88ee7f259 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Types.ts @@ -0,0 +1,1190 @@ +/** + * Type-level utility types for TypeScript. + * + * This module provides generic type aliases used throughout the Effect + * ecosystem. Everything here is compile-time only — there are no runtime + * values. Use these types to manipulate object shapes, tagged unions, tuples, + * and variance markers at the type level. + * + * ## Mental model + * + * - **Tagged union**: a union of objects each having a discriminating + * `_tag: string` field. {@link Tags}, {@link ExtractTag}, and + * {@link ExcludeTag} operate on these. + * - **Reason**: a nested error pattern where an error has a `reason` field + * containing a tagged union of sub-errors. {@link ReasonOf}, + * {@link ReasonTags}, {@link ExtractReason}, and {@link ExcludeReason} work + * with this pattern. + * - **Variance markers**: {@link Covariant}, {@link Contravariant}, and + * {@link Invariant} are function-type aliases encoding variance for phantom + * type parameters. + * - **Simplify**: {@link Simplify} flattens intersection types (`A & B`) into + * a single object type for cleaner IDE tooltips. + * - **Concurrency**: {@link Concurrency} is a union type + * (`number | "unbounded" | "inherit"`) used across Effect APIs that accept + * concurrency options. + * - **Marker types**: {@link unassigned} and {@link unhandled} are branded + * interfaces used internally to represent missing or unhandled type + * parameters. + * + * ## Common tasks + * + * - Flatten an intersection for readability → {@link Simplify} + * - Check type equality at compile time → {@link Equals} / {@link EqualsWith} + * - Merge two object types → {@link MergeLeft} / {@link MergeRight} + * - Work with tagged unions → {@link Tags} / {@link ExtractTag} / {@link ExcludeTag} + * - Work with nested reason errors → {@link ReasonOf} / {@link ExtractReason} + * - Create fixed-length tuples → {@link TupleOf} / {@link TupleOfAtLeast} + * - Strip `readonly` modifiers → {@link Mutable} / {@link DeepMutable} + * - Encode variance in phantom types → {@link Covariant} / {@link Contravariant} / {@link Invariant} + * - Check if a type is a union → {@link IsUnion} + * + * ## Gotchas + * + * - {@link TupleOf} with a non-literal `number` (e.g. `TupleOf`) + * degrades to `Array`. + * - {@link MergeRecord} is an alias for {@link MergeLeft}; prefer + * {@link MergeLeft} or {@link MergeRight} for clarity. + * - {@link NoInfer} uses the `[A][A extends any ? 0 : never]` trick, not the + * built-in `NoInfer` from TypeScript 5.4+. + * - {@link DeepMutable} recurses into `Map`, `Set`, arrays, and objects but + * stops at primitives and functions. + * + * @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** (Fixed-length tuple) + * + * ```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** (Minimum-length tuple) + * + * ```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 narrow a union by removing a specific variant. + * + * **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 narrow a union down to a single variant. + * + * **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** (Union to 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** (Type equality check) + * + * ```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** (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 conditionally branch based on the presence of keys in a type. + * + * **Details** + * + * Returns `true` if at least one key from `Key` exists in `A`, `false` + * otherwise. + * + * **Example** (Key presence check) + * + * ```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** (Left-biased merge) + * + * ```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 MergeRecord} + * @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] + } +> + +/** + * Alias for {@link MergeLeft}. Merges two object types where keys from + * `Source` take precedence on conflict. + * + * **When to use** + * + * Use when prefer {@link MergeLeft} or {@link MergeRight} for clarity about which + * side wins. + * + * **Example** (Merging records) + * + * ```ts + * import type { Types } from "effect" + * + * type Result = Types.MergeRecord< + * { a: number; b: number }, + * { a: string; c: boolean } + * > + * // { a: number; b: number; c: boolean } + * ``` + * + * @see {@link MergeLeft} + * @see {@link MergeRight} + * + * @category models + * @since 2.0.0 + */ +export type MergeRecord = MergeLeft + +/** + * 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** (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** (Shallow mutable conversion) + * + * ```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** (Deep mutable conversion) + * + * ```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** (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** (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** (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 with the nested error pattern where errors wrap sub-errors in a `reason` + * field. + * + * **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 to extract only the matching reason variant from a nested 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 to remove a handled reason variant from an error's reason union. + * + * **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-smol/packages/effect/src/UndefinedOr.ts b/.repos/effect-smol/packages/effect/src/UndefinedOr.ts new file mode 100644 index 00000000000..ee814a41513 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/UndefinedOr.ts @@ -0,0 +1,285 @@ +/** + * The `UndefinedOr` module works with plain TypeScript values of type + * `A | undefined` where `undefined` is the only absence marker. It is a small + * alternative to wrapping values in `Option` when your domain already uses + * `undefined` to mean "no value". + * + * **Mental model** + * + * - A defined value is the present branch. + * - `undefined` is reserved for absence; the payload type `A` should not itself + * include `undefined`. + * - Helpers such as {@link map} and {@link match} keep values unwrapped and use + * `undefined` directly instead of allocating tagged values. + * - The fail-fast combiner and reducer helpers propagate `undefined` when a + * required value is missing. + * + * **Common tasks** + * + * - Transform a present value: {@link map} + * - Branch on present versus absent: {@link match} + * - Unwrap at a boundary: {@link getOrThrow}, {@link getOrThrowWith} + * - Adapt throwing code to an optional result: {@link liftThrowable} + * - Lift combination and reduction logic: {@link makeReducer}, + * {@link makeCombinerFailFast}, {@link makeReducerFailFast} + * + * **Gotchas** + * + * - `A | undefined` cannot distinguish "missing" from "present and + * undefined". Use `Option` when that distinction matters. + * - {@link liftThrowable} discards the thrown value and also returns + * `undefined` when the wrapped function throws. + * + * **Example** (Parsing optional input) + * + * ```ts + * import { UndefinedOr } from "effect" + * + * const parseInteger = UndefinedOr.liftThrowable((input: string) => { + * const value = Number.parseInt(input, 10) + * if (Number.isNaN(value)) { + * throw new Error("not an integer") + * } + * return value + * }) + * + * console.log(UndefinedOr.map(parseInteger("42"), (n) => n + 1)) + * // 43 + * + * console.log( + * UndefinedOr.match(parseInteger("abc"), { + * onUndefined: () => "missing", + * onDefined: (n) => `parsed ${n}` + * }) + * ) + * // "missing" + * ``` + * + * @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 fail-fast unwrapping of an `A | undefined` value is appropriate and + * callers need 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 a value should already be defined at this point and throwing 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 to take the first available value like a fallback chain, combining values + * only when both operands are present. + * + * **Details** + * + * - `undefined` + `undefined` -> `undefined` + * - `a` + `undefined` -> `a` (first value wins) + * - `undefined` + `b` -> `b` (second value wins) + * - `a` + `b` -> `combiner.combine(a, b)` + * - 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-smol/packages/effect/src/Unify.ts b/.repos/effect-smol/packages/effect/src/Unify.ts new file mode 100644 index 00000000000..a40380c4f28 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Unify.ts @@ -0,0 +1,335 @@ +/** + * The `Unify` module defines the type-level protocol Effect uses to collapse + * unions of protocol-enabled values into their public data types. It is mostly + * for maintainers of Effect data types and advanced library authors; ordinary + * application code usually benefits from unification through APIs such as + * `Effect`, `Option`, `Result`, `Stream`, `Layer`, and `Match`. + * + * **Mental model** + * + * A type opts in by carrying phantom entries keyed by {@link typeSymbol} and + * {@link unifySymbol}. {@link Unify} reads those entries, ignores any protocol + * members listed through {@link ignoreSymbol}, and widens matching union + * members to the public type each entry returns. The runtime helper + * {@link unify} is an identity function; it changes only the static type that + * TypeScript sees. + * + * **Common tasks** + * + * - Add unification support to a new Effect data type so mixed unions infer as + * the public container type instead of an implementation shape. + * - Normalize the return type of branching APIs, matchers, or builders that can + * produce several protocol-enabled values. + * - Apply unification to a value or curried function result with {@link unify} + * while preserving the original runtime behavior. + * + * **Gotchas** + * + * - Unification is a compile-time protocol, not a runtime conversion hook. + * - Protocol entries should be specific to the data type they widen; overly + * broad entries can make inferred unions less precise. + * + * @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 with `unifySymbol` to expose the source type that unification should read + * 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 utils + * @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-smol/packages/effect/src/Utils.ts b/.repos/effect-smol/packages/effect/src/Utils.ts new file mode 100644 index 00000000000..55a019318a3 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/Utils.ts @@ -0,0 +1,261 @@ +/** + * Internal utilities for the Effect ecosystem's generator-based syntax and + * higher-kinded type machinery. + * + * ## Mental model + * + * - **SingleShotGen** — an `IterableIterator` wrapper that yields its value + * exactly once. Used internally by `[Symbol.iterator]()` on Effect, Option, + * Result, and other yieldable types so they work inside generator functions. + * - **Gen** — a type-level signature for generator-based monadic composition + * (`gen` functions). Parametric over any `TypeLambda` so each module + * (Effect, Option, Result, ...) can expose its own `gen` with correct types. + * - **Variance** — a type-level marker that encodes the variance (covariant, + * contravariant, invariant) of a `TypeLambda`'s type parameters. + * Used by {@link Gen} for type inference. + * + * ## Common tasks + * + * - Make a type yieldable in generators -> implement `[Symbol.iterator]()` returning a {@link SingleShotGen} + * - Define a generator-based API for a new TypeLambda -> type it as {@link Gen}`` + * - Encode variance for a higher-kinded type -> use {@link Variance} + * + * ## Gotchas + * + * - {@link SingleShotGen} yields its value only on the first `.next()` call. + * Calling `.next()` again returns `{ done: true }`. Iterating the same + * instance twice will skip the value on the second pass; call + * `[Symbol.iterator]()` to get a fresh iterator. + * - {@link Gen} and {@link Variance} are pure type-level constructs — they + * have no runtime representation. + * + * ## Quickstart + * + * **Example** (Using SingleShotGen to make a type yieldable) + * + * ```ts + * import { Utils } from "effect" + * + * class MyWrapper { + * constructor(readonly value: A) {} + * [Symbol.iterator]() { + * return new Utils.SingleShotGen, A>(this) + * } + * } + * + * const w = new MyWrapper(42) + * const iter = w[Symbol.iterator]() + * console.log(iter.next(undefined as any)) + * // { value: MyWrapper { value: 42 }, done: false } + * console.log(iter.next(42)) + * // { value: 42, done: true } + * ``` + * + * @see {@link SingleShotGen} + * @see {@link Gen} + * @see {@link Variance} + * + * @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 +> + +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 + +/** @internal */ +export const internalCall = isNotOptimizedAway ? standard[InternalTypeId] : forced[InternalTypeId] diff --git a/.repos/effect-smol/packages/effect/src/index.ts b/.repos/effect-smol/packages/effect/src/index.ts new file mode 100644 index 00000000000..0fae70acc8e --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/index.ts @@ -0,0 +1,712 @@ +/** + * @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 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 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-smol/packages/effect/src/internal/array.ts b/.repos/effect-smol/packages/effect/src/internal/array.ts new file mode 100644 index 00000000000..9f5100e3365 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/src/internal/concurrency.ts b/.repos/effect-smol/packages/effect/src/internal/concurrency.ts new file mode 100644 index 00000000000..480cd73a440 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/internal/concurrency.ts @@ -0,0 +1,52 @@ +import type { Effect } from "../Effect.ts" +import { CurrentConcurrency } from "../References.ts" +import type { Concurrency } from "../Types.ts" +import * as effect from "./effect.ts" + +/** @internal */ +export const match = ( + concurrency: Concurrency | undefined, + sequential: () => Effect, + unbounded: () => Effect, + bounded: (limit: number) => Effect +): Effect => { + switch (concurrency) { + case undefined: + return sequential() + case "unbounded": + return unbounded() + case "inherit": + return effect.flatMap(CurrentConcurrency, (concurrency) => + concurrency === "unbounded" + ? unbounded() + : concurrency > 1 + ? bounded(concurrency) + : sequential()) + default: + return concurrency > 1 ? bounded(concurrency) : sequential() + } +} + +/** @internal */ +export const matchSimple = ( + concurrency: Concurrency | undefined, + sequential: () => Effect, + concurrent: () => Effect +): Effect => { + switch (concurrency) { + case undefined: + return sequential() + case "unbounded": + return concurrent() + case "inherit": + return effect.flatMap( + CurrentConcurrency, + (concurrency) => + concurrency === "unbounded" || concurrency > 1 + ? concurrent() + : sequential() + ) + default: + return concurrency > 1 ? concurrent() : sequential() + } +} diff --git a/.repos/effect-smol/packages/effect/src/internal/core.ts b/.repos/effect-smol/packages/effect/src/internal/core.ts new file mode 100644 index 00000000000..f0a8c431689 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/src/internal/dateTime.ts b/.repos/effect-smol/packages/effect/src/internal/dateTime.ts new file mode 100644 index 00000000000..63a9c2cd8b9 --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/src/internal/doNotation.ts b/.repos/effect-smol/packages/effect/src/internal/doNotation.ts new file mode 100644 index 00000000000..7c6e3c919bb --- /dev/null +++ b/.repos/effect-smol/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-smol/packages/effect/src/internal/effect.ts b/.repos/effect-smol/packages/effect/src/internal/effect.ts new file mode 100644 index 00000000000..08cce736d30 --- /dev/null +++ b/.repos/effect-smol/packages/effect/src/internal/effect.ts @@ -0,0 +1,6426 @@ +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 { addSpanStackTrace, type ErrorWithStackTraceLimit, 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): Array => { + const errors: Array = [] + const interrupts: Array = [] + if (self.reasons.length === 0) return errors + + const prevStackLimit = (Error as ErrorWithStackTraceLimit).stackTraceLimit + ;(Error as ErrorWithStackTraceLimit) + .stackTraceLimit = 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 + ) + ) + } + 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)) + } + + ;(Error as ErrorWithStackTraceLimit).stackTraceLimit = prevStackLimit + return errors +} + +/** @internal */ +export const causePrettyError = ( + original: Record | Error, + annotations?: ReadonlyMap +): 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 + } + 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((e) => + e.cause ? `${e.stack} {\n${renderErrorCause(e.cause as Error, " ")}\n}` : e.stack + ) + .join("\n") + +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.currentLoopCount = 0 + this.interruptible = interruptible + this._stack = [] + this._observers = [] + this._exit = undefined + this._children = undefined + this._interruptedCause = undefined + this._yielded = undefined + this.runtimeMetrics?.recordFiberStart(this.context) + } + + readonly [FiberTypeId]: Fiber.Fiber.Variance + + readonly id: number + interruptible: boolean + currentOpCount: number + currentLoopCount: 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 + + // 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) { + 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 + let yielding = false + let current: Primitive | Yield = effect + this.currentOpCount = 0 + const currentLoop = ++this.currentLoopCount + try { + while (true) { + 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 (currentLoop !== this.currentLoopCount) { + // another effect has taken over the loop, + return Yield + } else if (current === Yield) { + const yielded = this._yielded! + if (ExitTypeId in yielded) { + this._yielded = undefined + return yielded + } + 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 { + ;(globalThis as any)[currentFiberTypeId] = prevFiber + } + } + getCont(symbol: S): + | (Primitive & Record Primitive>) + | undefined + { + 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 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) + for (const fiber of fibers) { + fiber.interruptUnsafe(parent.id, annotations) + } + return asVoid(fiberAwaitAll(fibers)) + }) + +/** @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) + for (const fiber of fibers) fiber.interruptUnsafe(fiberId, annotations) + return asVoid(fiberAwaitAll(fibers)) + })) + +/** @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: (option: Option.Option) => Effect.Effect = Option.match({ + onNone: () => fail(new NoSuchElementError("Effect.fromOption: Option.none")), + onSome: succeed +}) + +/** @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 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: { + try: LazyArg + catch: (error: unknown) => E +}): Effect.Effect => + suspend(() => { + try { + return succeed(internalCall(options.try)) + } catch (err) { + return fail(internalCall(() => options.catch(err))) + } + }) +/** @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) { + try { + internalCall(() => f(signal!)).then( + (a) => resume(succeed(a)), + (e) => resume(fail(internalCall(() => catcher(e)) as E)) + ) + } catch (err) { + resume(fail(internalCall(() => catcher(err)) as E)) + } + }, eval.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 = globalThis.Error.stackTraceLimit + globalThis.Error.stackTraceLimit = 2 + const defError = new globalThis.Error() + globalThis.Error.stackTraceLimit = 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 = globalThis.Error.stackTraceLimit + globalThis.Error.stackTraceLimit = 2 + const callError = new globalThis.Error() + globalThis.Error.stackTraceLimit = 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 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 mapErrorCause: { + ( + f: (e: Cause.Cause) => Cause.Cause + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (e: Cause.Cause) => Cause.Cause + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (e: Cause.Cause) => Cause.Cause + ): Effect.Effect => catchCause(self, (cause) => failCauseSync(() => f(cause))) +) + +/** @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 + ) + }) + +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?: { + readonly concurrency?: number | undefined + readonly start?: number | undefined + readonly end?: number | undefined + } +) => Effect.Effect | undefined => { + const onItem = options.onItem + const step = options.step + + return ( + state: S, + items: ReadonlyArray, + opts: { + readonly concurrency?: number | undefined + readonly start?: number | undefined + readonly end?: number | undefined + } | undefined + ): Effect.Effect | undefined => { + let index = opts?.start ?? 0 + const end = opts?.end ?? items.length + const concurrency = opts?.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 + + 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 = step(state, item, eff, index) + if (terminal) break + + // Use flatMap for concurrency of 1 + } else if (concurrency === 1) { + return flatMap(exit(eff), (exit) => { + terminal = step(state, 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 = step(state, 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 = step(state, 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?: { + readonly concurrency?: number | undefined + readonly start?: number | undefined + readonly end?: number | undefined + } +) => 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) +} + +/** @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 { + const millis = Duration.toMillis(duration) + if (millis <= 0) return yieldNow + return callback((resume) => { + if (millis > MAX_TIMER_MILLIS) return + const handle = setTimeout(() => resume(void_), 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 + } else if (typeof performance.timeOrigin === "number" && performance.timeOrigin === 0) { + return () => BigInt(Math.round(performance.now() * 1_000_000)) + } + const 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 = performanceNowNanos() - 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