diff --git a/.changeset/effect-main-rc-apis.md b/.changeset/effect-main-rc-apis.md new file mode 100644 index 0000000000..c526da8525 --- /dev/null +++ b/.changeset/effect-main-rc-apis.md @@ -0,0 +1,14 @@ +--- +"effect-app": patch +"@effect-app/cli": patch +--- + +Adapt to unpublished Effect main (pkg.pr.new `addeaea`, still versioned `4.0.0-rc.112`). + +npm `rc` remains `4.0.0-rc.112`; this pin consumes Effect main until the next RC publishes. Breaking API updates: + +- PascalCase Config/CLI constructors (`Config.String`, `Flag.File`, `Config.NonEmptyString`, `Config.Redacted`, `Config.Literal`) +- `Config.Record` now returns a Config +- Schema `toArbitrary` (fast-check) replaced by native `effect/unstable/arbitrary` +- `Fiber.currentSpan` moved to `fiber.cache.span` +- HTTP server addresses are `InetAddressV4`/`InetAddressV6` instead of `TcpAddress` diff --git a/package.json b/package.json index 443358225d..f7286af547 100644 --- a/package.json +++ b/package.json @@ -50,15 +50,15 @@ "@effect-app/infra": "workspace:*", "@effect/language-service": "0.86.2", "@effect/tsgo": "^0.31.0", - "@effect/platform-node": "4.0.0-rc.112", - "@effect/vitest": "4.0.0-rc.112", + "@effect/platform-node": "https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", + "@effect/vitest": "https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", "@tsconfig/strictest": "^2.0.8", "@types/node": "25.9.1", "@typescript-eslint/eslint-plugin": "8.60.0", "@typescript-eslint/parser": "8.60.0", "@typescript/native-preview": "7.0.0-dev.20260626.1", "dprint": "^0.54.0", - "effect": "4.0.0-rc.112", + "effect": "https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", "effect-app": "workspace:*", "eslint": "^10.4.1", "json5": "^2.2.3", diff --git a/packages/cli/package.json b/packages/cli/package.json index e0b8cbcc50..78b76d2a09 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -14,8 +14,8 @@ "effect-app-cli": "./bin.js" }, "dependencies": { - "@effect/platform-node": "4.0.0-rc.112", - "effect": "4.0.0-rc.112", + "@effect/platform-node": "https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", + "effect": "https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", "js-yaml": "4.2.0" }, "devDependencies": { diff --git a/packages/cli/src/gist.ts b/packages/cli/src/gist.ts index c437ad960a..272e6fd75d 100644 --- a/packages/cli/src/gist.ts +++ b/packages/cli/src/gist.ts @@ -547,9 +547,9 @@ export class GistHandler extends Context.Service()("GistHandler", { handler: Effect.fn("effa-cli.gist.GistHandler")(function*({ YAMLPath }: { YAMLPath: string }) { // load company and environment from environment variables const CONFIG = yield* Config.all({ - company: Config.string("COMPANY"), - env: Config.string("ENV").pipe(Config.withDefault("local-dev")), - gistCacheId: Config.nonEmptyString("EFFA_GIST_CACHE_ID") + company: Config.String("COMPANY"), + env: Config.String("ENV").pipe(Config.withDefault("local-dev")), + gistCacheId: Config.NonEmptyString("EFFA_GIST_CACHE_ID") }) yield* Effect.logInfo(`Company: ${CONFIG.company}, ENV: ${CONFIG.env}`) @@ -576,7 +576,7 @@ export class GistHandler extends Context.Service()("GistHandler", { ) // load GitHub token securely from environment variable - const redactedToken = yield* Config.redacted(configFromYaml.settings.token_env) + const redactedToken = yield* Config.Redacted(configFromYaml.settings.token_env) yield* Effect.logInfo(`Using GitHub token from environment variable: ${configFromYaml.settings.token_env}`) yield* Effect.logInfo(`Token loaded: ${redactedToken}`) // this will show in logs diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a36611456a..ff0b07779b 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -403,7 +403,7 @@ NodeRuntime.runMain( * CLI */ - const WrapAsOption = Flag.string("wrap").pipe( + const WrapAsOption = Flag.String("wrap").pipe( Flag.withAlias("w"), Flag.optional, Flag.withDescription( @@ -413,7 +413,7 @@ NodeRuntime.runMain( // has prio over WrapAsOption const WrapAsArg = Argument - .string("wrap") + .String("wrap") .pipe( Argument.atLeast(1), Argument.optional, @@ -473,7 +473,7 @@ NodeRuntime.runMain( ) const EffectAppLibsPath = Argument - .directory("effect-app-libs-path", { mustExist: true }) + .Directory("effect-app-libs-path", { mustExist: true }) .pipe( Argument.withDefault("../../effect-app/libs"), Argument.withDescription("Path to the effect-app-libs directory") @@ -506,7 +506,7 @@ NodeRuntime.runMain( Effect.fn("effa-cli.ue")(function*({}) { yield* Effect.logInfo("Update effect-app and/or effect packages") - const prompted = yield* Prompt.select({ + const prompted = yield* Prompt.Select({ choices: [ { title: "effect-app", @@ -665,7 +665,7 @@ NodeRuntime.runMain( .make( "gist", { - config: Flag.file("config").pipe( + config: Flag.File("config").pipe( Flag.withDefault("gists.yaml"), Flag.withDescription("Path to YAML configuration file") ) @@ -683,10 +683,10 @@ NodeRuntime.runMain( .make( "nuke", { - dryRun: Flag.boolean("dry-run").pipe( + dryRun: Flag.Boolean("dry-run").pipe( Flag.withDescription("Show what would be done without making changes") ), - storePrune: Flag.boolean("store-prune").pipe( + storePrune: Flag.Boolean("store-prune").pipe( Flag.withDescription("Prune the package manager store") ) }, @@ -718,18 +718,18 @@ NodeRuntime.runMain( .make( "sync-effect", { - manifests: Flag.string("manifests").pipe( + manifests: Flag.String("manifests").pipe( Flag.withAlias("m"), Flag.optional, Flag.withDescription( "Comma-separated list of package.json paths to scan (default: package.json)" ) ), - prefix: Flag.string("prefix").pipe( + prefix: Flag.String("prefix").pipe( Flag.optional, Flag.withDescription("Subtree prefix (default: repos/effect)") ), - url: Flag.string("url").pipe( + url: Flag.String("url").pipe( Flag.optional, Flag.withDescription( "Git repository URL (default: https://github.com/Effect-TS/effect.git)" @@ -753,24 +753,24 @@ NodeRuntime.runMain( .make( "sync-effect-app", { - manifests: Flag.string("manifests").pipe( + manifests: Flag.String("manifests").pipe( Flag.withAlias("m"), Flag.optional, Flag.withDescription( "Comma-separated list of package.json paths to scan (default: package.json)" ) ), - prefix: Flag.string("prefix").pipe( + prefix: Flag.String("prefix").pipe( Flag.optional, Flag.withDescription("Subtree prefix (default: repos/libs)") ), - url: Flag.string("url").pipe( + url: Flag.String("url").pipe( Flag.optional, Flag.withDescription( "Git repository URL (default: https://github.com/effect-app/libs.git)" ) ), - ref: Flag.string("ref").pipe( + ref: Flag.String("ref").pipe( Flag.optional, Flag.withDescription("Ref escape hatch (branch/tag/sha/latest); latest means main") ) @@ -792,7 +792,7 @@ NodeRuntime.runMain( ) .pipe(Command.withDescription("Sync the Effect App libs subtree to the version pinned in package.json")) - const SharedLockfileFlag = Flag.file("lockfile").pipe( + const SharedLockfileFlag = Flag.File("lockfile").pipe( Flag.optional, Flag.withDescription("Path to lockfile (default: .shared.json)") ) @@ -802,10 +802,10 @@ NodeRuntime.runMain( "sync", { lockfile: SharedLockfileFlag, - update: Flag.boolean("update").pipe( + update: Flag.Boolean("update").pipe( Flag.withDescription("Bump the pinned ref to the latest sha before syncing") ), - ref: Flag.string("ref").pipe( + ref: Flag.String("ref").pipe( Flag.optional, Flag.withDescription("Ref (branch/tag/sha) to update to; default: remote default branch HEAD") ) @@ -839,16 +839,16 @@ NodeRuntime.runMain( "sync-push", { lockfile: SharedLockfileFlag, - message: Flag.string("message").pipe( + message: Flag.String("message").pipe( Flag.withAlias("m"), Flag.optional, Flag.withDescription("Commit message for the push") ), - branch: Flag.string("branch").pipe( + branch: Flag.String("branch").pipe( Flag.optional, Flag.withDescription("Branch name in shared repo (default: auto-generated)") ), - pr: Flag.boolean("pr").pipe( + pr: Flag.Boolean("pr").pipe( Flag.withDescription("Open a PR via `gh pr create` after pushing") ) }, diff --git a/packages/e2e/package.json b/packages/e2e/package.json index 46ac8c208a..9943547400 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -10,13 +10,13 @@ "effect-app": "workspace:*" }, "devDependencies": { - "@effect/atom-vue": "^4.0.0-rc.112", - "@effect/platform-node": "4.0.0-rc.112", - "@effect/vitest": "4.0.0-rc.112", + "@effect/atom-vue": "https://pkg.pr.new/Effect-TS/effect/@effect/atom-vue@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", + "@effect/platform-node": "https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", + "@effect/vitest": "https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", "@tanstack/vue-query": "5.96.2", "@types/node": "25.9.1", "@vitejs/plugin-vue": "^6.0.7", - "effect": "^4.0.0-rc.112", + "effect": "https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", "typescript": "~6.0.3", "vitest": "^4.1.7", "vue": "^3.5.35" diff --git a/packages/e2e/test/repoInvalidation.e2e.test.ts b/packages/e2e/test/repoInvalidation.e2e.test.ts index 7247e22801..6091c9dbb9 100644 --- a/packages/e2e/test/repoInvalidation.e2e.test.ts +++ b/packages/e2e/test/repoInvalidation.e2e.test.ts @@ -19,6 +19,7 @@ import * as ManagedRuntime from "effect/ManagedRuntime" import * as Option from "effect/Option" import * as Scope from "effect/Scope" import { FetchHttpClient } from "effect/unstable/http" +import * as NetAddress from "effect/unstable/net/NetAddress" import * as Reactivity from "effect/unstable/reactivity/Reactivity" import { RpcSerialization } from "effect/unstable/rpc" import { createServer } from "http" @@ -143,8 +144,10 @@ const ClientLayer = Layer Effect.gen(function*() { const server = yield* HttpServer.HttpServer const addr = server.address - if (addr._tag !== "TcpAddress") return yield* Effect.die(new Error("expected TcpAddress")) - const host = addr.hostname === "0.0.0.0" ? "127.0.0.1" : addr.hostname + if (NetAddress.isUnixPathAddress(addr)) { + return yield* Effect.die(new Error("expected inet address")) + } + const host = NetAddress.isUnspecified(addr.address) ? "127.0.0.1" : NetAddress.formatUrlHost(addr.address) return ApiClientFactory .layer({ url: `http://${host}:${addr.port}`, headers: Option.none() }) .pipe(Layer.provide(FetchHttpClient.layer)) diff --git a/packages/effect-app/package.json b/packages/effect-app/package.json index 741edcb055..5a288a6921 100644 --- a/packages/effect-app/package.json +++ b/packages/effect-app/package.json @@ -18,6 +18,7 @@ "validator": "^13.15.35" }, "devDependencies": { + "effect": "https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", "@faker-js/faker": "^8.4.1", "@types/node": "25.9.1", "@types/validator": "^13.15.10", diff --git a/packages/effect-app/src/Config/SecretURL.ts b/packages/effect-app/src/Config/SecretURL.ts index 044f4bc7ea..684db9396c 100644 --- a/packages/effect-app/src/Config/SecretURL.ts +++ b/packages/effect-app/src/Config/SecretURL.ts @@ -76,5 +76,5 @@ export const value: (self: SecretURL) => string = internal.value export const unsafeWipe: (self: SecretURL) => void = internal.unsafeWipe export const secretURL = (name?: string): Config.Config => { - return Config.map(Config.nonEmptyString(name), fromString) + return Config.map(Config.NonEmptyString(name), fromString) } diff --git a/packages/effect-app/src/Model/query/new-kid-interpreter.ts b/packages/effect-app/src/Model/query/new-kid-interpreter.ts index 8c55a19388..e3c6804446 100644 --- a/packages/effect-app/src/Model/query/new-kid-interpreter.ts +++ b/packages/effect-app/src/Model/query/new-kid-interpreter.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { identity, pipe } from "effect/Function" -import * as Match from "effect/Match" +import { identity } from "effect/Function" import * as Array from "../../Array.ts" import { toNonEmptyArray } from "../../Array.ts" import * as Option from "../../Option.ts" @@ -192,197 +191,204 @@ const interpret = < ? { ..._, path: `${path}.-1.${_.path}` } : { ..._, result: _.result.map(applyPath(path)) } - pipe( - a, - Match.valueTags({ - value: () => { - // data.filter.push(value) - }, - where: ({ current, operation, relation, subPath }) => { - upd(interpret(current)) - if (typeof operation === "function") { - data.filter.push( - { - t: "where-scope", - result: interpret(operation(make())).filter.map(subPath ? applyPath(subPath) : identity), - relation - } - ) - } else { - data.filter.push( - { - t: "where", - path: operation[0], - op: operation.length === 2 ? "eq" : operation[1], - value: operation.length === 2 ? operation[1] : operation[2] - } - ) - } - }, - and: ({ current, operation, relation }) => { - upd(interpret(current)) - if (typeof operation === "function") { - data.filter.push( - { t: "and-scope", result: interpret(operation(make())).filter, relation } - ) - } else { - data.filter.push( - { - t: "and", - path: operation[0], - op: operation.length === 2 ? "eq" : operation[1], - value: operation.length === 2 ? operation[1] : operation[2] - } - ) - } - }, - or: ({ current, operation, relation }) => { - upd(interpret(current)) - if (typeof operation === "function") { - data.filter.push( - { t: "or-scope", result: interpret(operation(make())).filter, relation } - ) - } else { - data.filter.push( - { - t: "or", - path: operation[0], - op: operation.length === 2 ? "eq" : operation[1], - value: operation.length === 2 ? operation[1] : operation[2] - } - ) - } - }, - one: ({ current }) => { - upd(interpret(current)) - data.limit = 1 - data.ttype = "one" - }, - count: ({ current }) => { - upd(interpret(current)) - data.ttype = "count" - data.schema = S.Struct({ id: S.String }) as any - }, - order: ({ current, direction, field }) => { - upd(interpret(current)) - data.order.push({ key: field, direction }) - }, - page: (v) => { - upd(interpret(v.current)) - data.limit = v.take - data.skip = v.skip - }, - project: (v) => { - upd(interpret(v.current)) - if (v.mode === "aggregate" && v.aggregateMap) { - data.schema = v.schema - data.mode = "aggregate" - data.aggregateMap = Object.fromEntries( - Object.entries(v.aggregateMap).map(([key, expression]) => { - switch (expression._tag) { - case "agg-field": - return [key, { _tag: "agg-field" as const, path: expression.path }] - case "agg-count": - return [key, { _tag: "agg-count" as const }] - case "agg-count-when": { - const filter = interpret(expression.operation(make())).filter - return [key, { _tag: "agg-count-when" as const, filter }] - } - case "agg-sum": - return [key, { _tag: "agg-sum" as const, field: expression.field }] - case "agg-min": - return [key, { _tag: "agg-min" as const, field: expression.field }] - case "agg-max": - return [key, { _tag: "agg-max" as const, field: expression.field }] - } - }) - ) - return - } - if (v.computed && v.mode === "transform") { - throw new Error("Computed projections require mode 'project' or 'collect', not 'transform'") - } - data.schema = v.schema - data.mode = v.computed - ? v.mode === "collect" ? "collect" : "project" - : v.mode - data.computed = v.computed - ? Object.fromEntries( - Object.entries(v.computed).map(([key, expression]) => { - const e = expression - const op = "operation" in e ? e.operation : undefined - const filter = op ? interpret(op(make())).filter.map(applyPath(e.path)) : [] - switch (e._tag) { - case "relation-count": - case "relation-any": - case "relation-every": - return [key, { _tag: e._tag, path: e.path, filter }] - case "relation-distinct-count": - case "relation-sum": - return [ - key, - { _tag: e._tag, path: e.path, field: e.field, filter } - ] - case "relation-sum-expr": - return [ - key, - { _tag: e._tag, path: e.path, expression: e.expression, filter } - ] - case "relation-sum-expr-by": - return [ - key, - { - _tag: e._tag, - path: e.path, - expression: e.expression, - unit: e.unit, - filter - } - ] - case "relation-sum-expr-normalized": - return [ - key, - { - _tag: e._tag, - path: e.path, - expression: e.expression, - unit: e.unit, - toBase: e.toBase, - factors: e.factors, - filter - } - ] - case "relation-collect": - return [ - key, - { - _tag: e._tag, - path: e.path, - field: e.field, - distinct: e.distinct, - filter - } - ] - case "relation-collect-fields": - return [ - key, - { - _tag: e._tag, - path: e.path, - fields: e.fields, - distinct: e.distinct, - filter - } - ] - case "relation-length": - return [key, { _tag: e._tag, path: e.path }] + switch (a._tag) { + case "value": + break + case "where": { + const { current, operation, relation, subPath } = a + upd(interpret(current)) + if (typeof operation === "function") { + data.filter.push( + { + t: "where-scope", + result: interpret(operation(make())).filter.map(subPath ? applyPath(subPath) : identity), + relation + } + ) + } else { + data.filter.push( + { + t: "where", + path: operation[0], + op: operation.length === 2 ? "eq" : operation[1], + value: operation.length === 2 ? operation[1] : operation[2] + } + ) + } + break + } + case "and": { + const { current, operation, relation } = a + upd(interpret(current)) + if (typeof operation === "function") { + data.filter.push( + { t: "and-scope", result: interpret(operation(make())).filter, relation } + ) + } else { + data.filter.push( + { + t: "and", + path: operation[0], + op: operation.length === 2 ? "eq" : operation[1], + value: operation.length === 2 ? operation[1] : operation[2] + } + ) + } + break + } + case "or": { + const { current, operation, relation } = a + upd(interpret(current)) + if (typeof operation === "function") { + data.filter.push( + { t: "or-scope", result: interpret(operation(make())).filter, relation } + ) + } else { + data.filter.push( + { + t: "or", + path: operation[0], + op: operation.length === 2 ? "eq" : operation[1], + value: operation.length === 2 ? operation[1] : operation[2] + } + ) + } + break + } + case "one": { + upd(interpret(a.current)) + data.limit = 1 + data.ttype = "one" + break + } + case "count": { + upd(interpret(a.current)) + data.ttype = "count" + data.schema = S.Struct({ id: S.String }) as any + break + } + case "order": { + upd(interpret(a.current)) + data.order.push({ key: a.field, direction: a.direction }) + break + } + case "page": { + upd(interpret(a.current)) + data.limit = a.take + data.skip = a.skip + break + } + case "project": { + upd(interpret(a.current)) + if (a.mode === "aggregate" && a.aggregateMap) { + data.schema = a.schema + data.mode = "aggregate" + data.aggregateMap = Object.fromEntries( + Object.entries(a.aggregateMap).map(([key, expression]) => { + switch (expression._tag) { + case "agg-field": + return [key, { _tag: "agg-field" as const, path: expression.path }] + case "agg-count": + return [key, { _tag: "agg-count" as const }] + case "agg-count-when": { + const filter = interpret(expression.operation(make())).filter + return [key, { _tag: "agg-count-when" as const, filter }] } - }) - ) - : undefined + case "agg-sum": + return [key, { _tag: "agg-sum" as const, field: expression.field }] + case "agg-min": + return [key, { _tag: "agg-min" as const, field: expression.field }] + case "agg-max": + return [key, { _tag: "agg-max" as const, field: expression.field }] + } + }) + ) + break } - }) - ) + if (a.computed && a.mode === "transform") { + throw new Error("Computed projections require mode 'project' or 'collect', not 'transform'") + } + data.schema = a.schema + data.mode = a.computed + ? a.mode === "collect" ? "collect" : "project" + : a.mode + data.computed = a.computed + ? Object.fromEntries( + Object.entries(a.computed).map(([key, expression]) => { + const e = expression + const op = "operation" in e ? e.operation : undefined + const filter = op ? interpret(op(make())).filter.map(applyPath(e.path)) : [] + switch (e._tag) { + case "relation-count": + case "relation-any": + case "relation-every": + return [key, { _tag: e._tag, path: e.path, filter }] + case "relation-distinct-count": + case "relation-sum": + return [ + key, + { _tag: e._tag, path: e.path, field: e.field, filter } + ] + case "relation-sum-expr": + return [ + key, + { _tag: e._tag, path: e.path, expression: e.expression, filter } + ] + case "relation-sum-expr-by": + return [ + key, + { + _tag: e._tag, + path: e.path, + expression: e.expression, + unit: e.unit, + filter + } + ] + case "relation-sum-expr-normalized": + return [ + key, + { + _tag: e._tag, + path: e.path, + expression: e.expression, + unit: e.unit, + toBase: e.toBase, + factors: e.factors, + filter + } + ] + case "relation-collect": + return [ + key, + { + _tag: e._tag, + path: e.path, + field: e.field, + distinct: e.distinct, + filter + } + ] + case "relation-collect-fields": + return [ + key, + { + _tag: e._tag, + path: e.path, + fields: e.fields, + distinct: e.distinct, + filter + } + ] + case "relation-length": + return [key, { _tag: e._tag, path: e.path }] + } + }) + ) + : undefined + break + } + } return data } diff --git a/packages/effect-app/src/Schema.ts b/packages/effect-app/src/Schema.ts index ebd79f40d8..a20a48b0da 100644 --- a/packages/effect-app/src/Schema.ts +++ b/packages/effect-app/src/Schema.ts @@ -2,10 +2,11 @@ import * as S from "effect/Schema" import { type Simplify } from "effect/Struct" import type * as Tracer from "effect/Tracer" import type { RequiredKeys } from "effect/Types" +import type { Arbitrary as FastCheckArbitrary } from "fast-check" import type { NonEmptyReadonlyArray } from "./Array.ts" -import { fakerArb } from "./faker.ts" import { Email as EmailT, type Email as EmailType } from "./Schema/email.ts" import { concurrencyUnbounded, withDefaultMake, withDefaultParseOptions } from "./Schema/ext.ts" +import type { FC } from "./Schema/FastCheck.ts" import { PhoneNumber as PhoneNumberT, type PhoneNumber as PhoneNumberType } from "./Schema/phoneNumber.ts" import { type AST } from "./Schema/schema.ts" import * as SchemaAST from "./SchemaAST.ts" @@ -129,6 +130,9 @@ export { NonEmptyString } from "./Schema/strings.ts" export * as SchemaIssue from "effect/SchemaIssue" +/** Fast-check generator factory previously exported as `Schema.Arbitrary`. */ +export type Arbitrary = (fc: FC) => FastCheckArbitrary + export const decodeEffectConcurrently: typeof S.decodeEffect = withDefaultParseOptions(S.decodeEffect) export const decodeUnknownEffectConcurrently: typeof S.decodeUnknownEffect = withDefaultParseOptions( S.decodeUnknownEffect @@ -347,29 +351,11 @@ export interface WithOptionalSpan { [SpanId]?: Tracer.Span } -const makeEmail = S.decodeSync(EmailT as any) as (value: string) => EmailType -const makePhoneNumber = S.decodeSync(PhoneNumberT as any) as (value: string) => PhoneNumberType - -export const Email = EmailT - .pipe( - S.annotate({ - // eslint-disable-next-line @typescript-eslint/unbound-method - toArbitrary: () => (fc) => fakerArb((faker) => faker.internet.exampleEmail)(fc).map(makeEmail) - }), - withDefaultMake - ) +export const Email = EmailT.pipe(withDefaultMake) export type Email = EmailType -export const PhoneNumber = PhoneNumberT - .pipe( - S.annotate({ - toArbitrary: () => (fc) => - // eslint-disable-next-line @typescript-eslint/unbound-method - fakerArb((faker) => faker.phone.number)(fc).map(makePhoneNumber) - }), - withDefaultMake - ) +export const PhoneNumber = PhoneNumberT.pipe(withDefaultMake) export type PhoneNumber = PhoneNumberType diff --git a/packages/effect-app/src/Schema/brand.ts b/packages/effect-app/src/Schema/brand.ts index 5f522daad9..73497ee741 100644 --- a/packages/effect-app/src/Schema/brand.ts +++ b/packages/effect-app/src/Schema/brand.ts @@ -28,12 +28,7 @@ export interface Constructor> { is(a: Unbranded): a is Unbranded & A } -type BrandAnnotations> = - & S.Annotations.Filter - & ( - C extends string ? { readonly toArbitrary?: S.Annotations.ToArbitrary.Declaration } - : {} - ) +type BrandAnnotations = S.Annotations.Filter export interface BrandedSchema> extends S.Bottom< @@ -57,7 +52,7 @@ export interface BrandedSchema> extends export const fromBrand = >( constructor: Constructor, - options?: BrandAnnotations + options?: BrandAnnotations ) => (self: Self): BrandedSchema => { const branded = S.fromBrand(options?.identifier ?? "Brand", constructor as any)(self as any) diff --git a/packages/effect-app/src/Schema/email.ts b/packages/effect-app/src/Schema/email.ts index 229c202b7d..dc1f933303 100644 --- a/packages/effect-app/src/Schema/email.ts +++ b/packages/effect-app/src/Schema/email.ts @@ -22,8 +22,5 @@ export const Email = S identifier: "Email", description: "an email according to RFC 5322", jsonSchema: { format: "email", minLength: 3, maxLength: 998 } - }), - S.annotate({ - toArbitrary: () => (fc) => fc.emailAddress().map((_) => _ as Email) }) ) diff --git a/packages/effect-app/src/Schema/ext.ts b/packages/effect-app/src/Schema/ext.ts index 232dbf2b0b..4f3e636034 100644 --- a/packages/effect-app/src/Schema/ext.ts +++ b/packages/effect-app/src/Schema/ext.ts @@ -54,8 +54,8 @@ type ProvidedCodec = S.Codec< const concurrencySetting = Effect.runSync( Config - .literal("unbounded", "SCHEMA_CONCURRENCY") - .pipe(Config.orElse(() => Config.number("SCHEMA_CONCURRENCY")), Config.option) + .Literal("unbounded", "SCHEMA_CONCURRENCY") + .pipe(Config.orElse(() => Config.Number("SCHEMA_CONCURRENCY")), Config.option) ) export const DefaultParseOptions: SchemaAST.ParseOptions = { diff --git a/packages/effect-app/src/Schema/moreStrings.ts b/packages/effect-app/src/Schema/moreStrings.ts index 22f3ef8ad6..d8ef9902ee 100644 --- a/packages/effect-app/src/Schema/moreStrings.ts +++ b/packages/effect-app/src/Schema/moreStrings.ts @@ -15,7 +15,7 @@ import * as Effect from "effect/Effect" import { pipe } from "effect/Function" import * as S from "effect/Schema" import type { Simplify } from "effect/Types" -import { customRandom, nanoid, urlAlphabet } from "nanoid" +import { nanoid } from "nanoid" import validator from "validator" import type * as SchemaAST from "../SchemaAST.ts" import { type BrandedSchema, fromBrand, nominal } from "./brand.ts" @@ -164,23 +164,16 @@ export type StringId = string & StringIdBrand const minLength = 6 const maxLength = 50 -const size = 21 -const length = 10 * size const StringIdSchemaBase = pipe( S.String, S.check(S.isMinLength(minLength), S.isMaxLength(maxLength)), fromBrand(nominal(), { identifier: "StringId", - toArbitrary: () => (fc) => StringIdArb()(fc), jsonSchema: {} }) ) const makeStringId = (s?: string): StringId => s !== undefined ? S.decodeSync(StringIdSchemaBase)(s) : nanoid() as unknown as StringId -const StringIdArb = (): S.Arbitrary => (fc) => - fc - .uint8Array({ minLength: length, maxLength: length }) - .map((_) => customRandom(urlAlphabet, size, (size) => _.subarray(0, size))() as StringId) /** * A string that is at least 6 characters long and a maximum of 50. * @@ -226,18 +219,11 @@ export function prefixedStringId() { ) => { type FullPrefix = `${Prefix}${Separator}` const pref = `${prefix}${separator ?? "-"}` as FullPrefix - const arb = (): S.Arbitrary => (fc) => - StringIdArb()(fc).map( - (x) => (pref + x.substring(0, 50 - pref.length)) as Type - ) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const s = StringIdSchemaBase .pipe( S.refine((x: string): x is Type => x.startsWith(pref), { identifier: name - }), - S.annotate({ - toArbitrary: () => (fc) => arb()(fc) }) ) const schema = s.pipe(withDefaultMake) @@ -331,8 +317,5 @@ export const Url: UrlSchema = S identifier: "Url", jsonSchema: { format: "uri" } }), - S.annotate({ - toArbitrary: () => (fc) => fc.webUrl().map((_) => _ as Url) - }), withDefaultMake ) diff --git a/packages/effect-app/src/Schema/phoneNumber.ts b/packages/effect-app/src/Schema/phoneNumber.ts index 1134ed11e0..2ddc9f6e66 100644 --- a/packages/effect-app/src/Schema/phoneNumber.ts +++ b/packages/effect-app/src/Schema/phoneNumber.ts @@ -3,7 +3,6 @@ import { isValidPhone } from "effect-app/validation" import * as S from "effect/Schema" import type { Simplify } from "effect/Types" import { withDefaultMake } from "./ext.ts" -import { Numbers } from "./FastCheck.ts" import type { B } from "./schema.ts" import type { NonEmptyStringBrand } from "./strings.ts" @@ -23,8 +22,5 @@ export const PhoneNumber = S description: "a phone number with at least 7 digits", jsonSchema: { format: "phone" } }), - S.annotate({ - toArbitrary: () => (fc) => Numbers(7, 10)(fc).map((_) => _ as PhoneNumber) - }), withDefaultMake ) diff --git a/packages/effect-app/src/client/apiClientFactory.ts b/packages/effect-app/src/client/apiClientFactory.ts index dfab8d5f57..b134362853 100644 --- a/packages/effect-app/src/client/apiClientFactory.ts +++ b/packages/effect-app/src/client/apiClientFactory.ts @@ -25,13 +25,8 @@ export interface ApiConfig { } export const DefaultApiConfig = Config.all({ - url: Config.string("apiUrl").pipe(Config.withDefault("/api")), - headers: Config - .schema( - Config.Record(Schema.String, Schema.String), - "headers" - ) - .pipe(Config.option) + url: Config.String("apiUrl").pipe(Config.withDefault("/api")), + headers: Config.Record(Schema.String, Schema.String, "headers").pipe(Config.option) }) export type Req = S.Top & { diff --git a/packages/effect-app/test/moreStrings.test.ts b/packages/effect-app/test/moreStrings.test.ts index a09f028a5f..2189c15c4c 100644 --- a/packages/effect-app/test/moreStrings.test.ts +++ b/packages/effect-app/test/moreStrings.test.ts @@ -1,5 +1,4 @@ import * as S from "effect-app/Schema" -import * as fc from "fast-check" import { urlAlphabet } from "nanoid" import { test } from "vitest" @@ -7,11 +6,10 @@ const nanoidAlphabet = new Set(urlAlphabet) const isNanoId = (value: string) => value.length === 21 && Array.from(value).every((char) => nanoidAlphabet.has(char)) -test("StringId arbitrary generates nanoid-shaped values", () => { - fc.assert( - fc.property(S.toArbitrary(S.StringId)(fc), (value) => { - expect(isNanoId(value)).toBe(true) - expect(S.is(S.StringId)(value)).toBe(true) - }) - ) +test("StringId make generates nanoid-shaped values", () => { + for (let i = 0; i < 20; i++) { + const value = S.StringId.make() + expect(isNanoId(value)).toBe(true) + expect(S.is(S.StringId)(value)).toBe(true) + } }) diff --git a/packages/infra/package.json b/packages/infra/package.json index c529c0c0f2..08dfe5a427 100644 --- a/packages/infra/package.json +++ b/packages/infra/package.json @@ -21,7 +21,7 @@ "devDependencies": { "@azure/cosmos": "^4.9.3", "@azure/service-bus": "^7.9.5", - "@effect/sql-sqlite-node": "4.0.0-rc.112", + "@effect/sql-sqlite-node": "https://pkg.pr.new/Effect-TS/effect/@effect/sql-sqlite-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", "@sentry/opentelemetry": "10.55.0", "@types/better-sqlite3": "^7.6.13", "@types/node": "25.9.1", @@ -33,6 +33,8 @@ "mongodb": "7.2.0", "redis": "^3.1.2", "redlock": "^4.2.0", + "effect": "https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", + "@effect/vitest": "https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", "typescript": "~6.0.3", "vitest": "^4.1.7" }, diff --git a/packages/infra/src/CUPS.ts b/packages/infra/src/CUPS.ts index cfa52c3f03..49c938311a 100644 --- a/packages/infra/src/CUPS.ts +++ b/packages/infra/src/CUPS.ts @@ -131,7 +131,7 @@ function* buildListArgs(config?: { host?: string | undefined }) { export const CUPSConfig = Config.all({ server: Config - .string("server") + .String("server") .pipe( Config.map((s) => new URL(s)), Config.option, diff --git a/packages/infra/src/arbs.ts b/packages/infra/src/arbs.ts index e3051ece94..0cd545b323 100644 --- a/packages/infra/src/arbs.ts +++ b/packages/infra/src/arbs.ts @@ -3,7 +3,9 @@ import { faker } from "@faker-js/faker" import { setFaker } from "effect-app/faker" import type * as S from "effect-app/Schema" -import * as FastCheck from "effect/testing/FastCheck" +import * as Effect from "effect/Effect" +import * as Arbitrary from "effect/unstable/arbitrary/Arbitrary" +import * as FastCheck from "fast-check" import { Random } from "fast-check" import { congruential32 } from "pure-rand/generator/congruential32" @@ -20,3 +22,14 @@ export function generate(arb: FastCheck.Arbitrary) { export function generateFromArbitrary(arb: S.Arbitrary) { return generate(arb(FastCheck)) } + +export function generateFromSchema(schema: S) { + const samples = Effect.runSync( + Arbitrary.sampleEffect(Arbitrary.schema(schema), { count: 1, seed }) + ) + const value = samples[0] + if (value === undefined) { + throw new Error("failed to sample schema") + } + return { value } +} diff --git a/packages/infra/src/logger/shared.ts b/packages/infra/src/logger/shared.ts index c79da9a4dc..60e8387cf7 100644 --- a/packages/infra/src/logger/shared.ts +++ b/packages/infra/src/logger/shared.ts @@ -5,7 +5,7 @@ import { storeId } from "effect-app/Store" import type * as Fiber from "effect/Fiber" export function getRequestContextFromFiber(fiber: Fiber.Fiber) { - const span = Option.fromNullishOr(fiber.currentSpan) + const span = Option.fromNullishOr(fiber.cache.span) const locale = fiber.getRef(LocaleRef) const namespace = fiber.getRef(storeId) return RequestContext.make({ diff --git a/packages/infra/src/routing.ts b/packages/infra/src/routing.ts index 438efbae80..edce4156cf 100644 --- a/packages/infra/src/routing.ts +++ b/packages/infra/src/routing.ts @@ -182,7 +182,7 @@ export type RouteMatcher< export const skipOnProd = Effect .gen(function*() { - const env = yield* Config.string("env") + const env = yield* Config.String("env") return env !== "prod" }) .pipe(Effect.orDie) diff --git a/packages/infra/src/routing/middleware/middleware.ts b/packages/infra/src/routing/middleware/middleware.ts index a549fb64a1..1b84d226d7 100644 --- a/packages/infra/src/routing/middleware/middleware.ts +++ b/packages/infra/src/routing/middleware/middleware.ts @@ -61,7 +61,7 @@ const summarizePayload = (payload: unknown): unknown => export const DevModeLive = Layer.effect( DevMode, Effect.gen(function*() { - const env = yield* Config.string("env").pipe(Config.withDefault("local-dev")) + const env = yield* Config.String("env").pipe(Config.withDefault("local-dev")) return env !== "prod" }) ) diff --git a/packages/infra/src/test.ts b/packages/infra/src/test.ts index 8d8e6d3134..ba59a3227e 100644 --- a/packages/infra/src/test.ts +++ b/packages/infra/src/test.ts @@ -1,12 +1,12 @@ import * as S from "effect-app/Schema" import { copy } from "effect-app/utils" -import { generateFromArbitrary } from "./arbs.ts" +import { generateFromSchema } from "./arbs.ts" /** * Given the schema for an object-like structure, creates a function that generates random instances of that object with some values provided. */ export const createRandomInstance = (s: S.Codec & { fields: S.Struct.Fields }) => { - const gen = generateFromArbitrary(S.toArbitrary(s)) + const gen = generateFromSchema(s) return (overrides?: Partial) => { const v = gen.value return overrides ? copy(v, overrides) : v @@ -17,7 +17,7 @@ export const createRandomInstance = (s: S.Codec * Like `createRandomInstance`, but takes encoded values rather than decoded ones. */ export const createRandomInstanceI = (s: S.Codec & { fields: S.Struct.Fields }) => { - const gen = generateFromArbitrary(S.toArbitrary(s)) + const gen = generateFromSchema(s) const encode = S.encodeSync(s) const decode = S.decodeSync(s) return (overrides?: Partial) => { diff --git a/packages/infra/test/rawQuery.test.ts b/packages/infra/test/rawQuery.test.ts index 8d2007ea82..f94900a6c1 100644 --- a/packages/infra/test/rawQuery.test.ts +++ b/packages/infra/test/rawQuery.test.ts @@ -99,7 +99,7 @@ class SomethingRepo extends Context.Service()( Layer.provide( Effect .gen(function*() { - const url = yield* Config.redacted("STORAGE_URL").pipe( + const url = yield* Config.Redacted("STORAGE_URL").pipe( Config.withDefault( Redacted.make( // the emulator doesn't implement array projections :/ so you need an actual cloud instance! diff --git a/packages/infra/test/rpc-context-map-streaming.test.ts b/packages/infra/test/rpc-context-map-streaming.test.ts index d0a332345e..462b483f9a 100644 --- a/packages/infra/test/rpc-context-map-streaming.test.ts +++ b/packages/infra/test/rpc-context-map-streaming.test.ts @@ -48,6 +48,7 @@ import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as Stream from "effect/Stream" import { FetchHttpClient } from "effect/unstable/http" +import * as NetAddress from "effect/unstable/net/NetAddress" import { RpcSerialization } from "effect/unstable/rpc" import { createServer } from "http" import { RequestContextMiddleware } from "../src/internal/RequestContextMiddleware.js" @@ -193,8 +194,10 @@ const ClientLayer = Layer Effect.gen(function*() { const server = yield* HttpServer.HttpServer const addr = server.address - if (addr._tag !== "TcpAddress") return yield* Effect.die(new Error("expected TcpAddress")) - const host = addr.hostname === "0.0.0.0" ? "127.0.0.1" : addr.hostname + if (NetAddress.isUnixPathAddress(addr)) { + return yield* Effect.die(new Error("expected inet address")) + } + const host = NetAddress.isUnspecified(addr.address) ? "127.0.0.1" : NetAddress.formatUrlHost(addr.address) const url = `http://${host}:${addr.port}` return ApiClientFactory .layer({ url, headers: Option.none() }) diff --git a/packages/infra/test/rpc-e2e-invalidation.test.ts b/packages/infra/test/rpc-e2e-invalidation.test.ts index 1f2010d1e9..88689c9b81 100644 --- a/packages/infra/test/rpc-e2e-invalidation.test.ts +++ b/packages/infra/test/rpc-e2e-invalidation.test.ts @@ -30,6 +30,7 @@ import * as Option from "effect/Option" import * as Ref from "effect/Ref" import * as Stream from "effect/Stream" import { FetchHttpClient } from "effect/unstable/http" +import * as NetAddress from "effect/unstable/net/NetAddress" import { RpcSerialization } from "effect/unstable/rpc" import { createServer } from "http" import { RequestContextMiddleware } from "../src/internal/RequestContextMiddleware.js" @@ -230,8 +231,10 @@ const ClientLayer = Layer Effect.gen(function*() { const server = yield* HttpServer.HttpServer const addr = server.address - if (addr._tag !== "TcpAddress") return yield* Effect.die(new Error("expected TcpAddress")) - const host = addr.hostname === "0.0.0.0" ? "127.0.0.1" : addr.hostname + if (NetAddress.isUnixPathAddress(addr)) { + return yield* Effect.die(new Error("expected inet address")) + } + const host = NetAddress.isUnspecified(addr.address) ? "127.0.0.1" : NetAddress.formatUrlHost(addr.address) const url = `http://${host}:${addr.port}` return ApiClientFactory .layer({ url, headers: Option.none() }) diff --git a/packages/infra/test/rpc-stream-fullstack.test.ts b/packages/infra/test/rpc-stream-fullstack.test.ts index e73aa53ac4..f2513da5c1 100644 --- a/packages/infra/test/rpc-stream-fullstack.test.ts +++ b/packages/infra/test/rpc-stream-fullstack.test.ts @@ -23,6 +23,7 @@ import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as Stream from "effect/Stream" import { FetchHttpClient } from "effect/unstable/http" +import * as NetAddress from "effect/unstable/net/NetAddress" import { RpcSerialization } from "effect/unstable/rpc" import { createServer } from "http" import { makeRouter } from "../src/routing.js" @@ -176,8 +177,10 @@ const ClientLayer = Layer Effect.gen(function*() { const server = yield* HttpServer.HttpServer const addr = server.address - if (addr._tag !== "TcpAddress") return yield* Effect.die(new Error("expected TcpAddress")) - const host = addr.hostname === "0.0.0.0" ? "127.0.0.1" : addr.hostname + if (NetAddress.isUnixPathAddress(addr)) { + return yield* Effect.die(new Error("expected inet address")) + } + const host = NetAddress.isUnspecified(addr.address) ? "127.0.0.1" : NetAddress.formatUrlHost(addr.address) const url = `http://${host}:${addr.port}` return ApiClientFactory .layer({ url, headers: Option.none() }) diff --git a/packages/vue-components/package.json b/packages/vue-components/package.json index 63a2be3c5e..4bdf4de36e 100644 --- a/packages/vue-components/package.json +++ b/packages/vue-components/package.json @@ -36,6 +36,7 @@ "vuetify": "^4.0.8" }, "devDependencies": { + "effect": "https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", "@effect-app/eslint-shared-config": "workspace:*", "@storybook/vue3": "^10.4.1", "@storybook/vue3-vite": "^10.4.1", diff --git a/packages/vue/package.json b/packages/vue/package.json index 5548fc4d78..0971761690 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -25,7 +25,10 @@ "vue": "^3.5.35" }, "devDependencies": { - "@effect/vitest": "4.0.0-rc.112", + "effect": "https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", + "@effect/atom-vue": "https://pkg.pr.new/Effect-TS/effect/@effect/atom-vue@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", + "@effect/platform-browser": "https://pkg.pr.new/Effect-TS/effect/@effect/platform-browser@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", + "@effect/vitest": "https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70", "@formatjs/icu-messageformat-parser": "^3.5.10", "@types/node": "25.9.1", "@vitejs/plugin-vue": "^6.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff9359f43b..4d1d2d5dad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,14 +45,14 @@ importers: specifier: 0.86.2 version: 0.86.2 "@effect/platform-node": - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1)) + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(redis@6.2.1(@opentelemetry/api@1.9.1)) "@effect/tsgo": specifier: ^0.31.0 version: 0.31.0 "@effect/vitest": - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))) + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))) "@tsconfig/strictest": specifier: ^2.0.8 version: 2.0.8 @@ -72,8 +72,8 @@ importers: specifier: ^0.54.0 version: 0.54.0 effect: - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + specifier: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 effect-app: specifier: workspace:* version: link:packages/effect-app @@ -117,11 +117,11 @@ importers: packages/cli: dependencies: "@effect/platform-node": - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1)) + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(redis@6.2.1(@opentelemetry/api@1.9.1)) effect: - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + specifier: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 js-yaml: specifier: 4.2.0 version: 4.2.0 @@ -153,14 +153,14 @@ importers: version: link:../effect-app devDependencies: "@effect/atom-vue": - specifier: ^4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e))) + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/atom-vue@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/atom-vue@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e))) "@effect/platform-node": - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1)) + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(redis@6.2.1(@opentelemetry/api@1.9.1)) "@effect/vitest": - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))) + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))) "@tanstack/vue-query": specifier: 5.96.2 version: 5.96.2(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e))) @@ -171,8 +171,8 @@ importers: specifier: ^6.0.7 version: 6.0.7(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e))) effect: - specifier: ^4.0.0-rc.112 - version: 4.0.0-rc.112 + specifier: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 typescript: specifier: ~6.0.3 version: 6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e) @@ -191,9 +191,6 @@ importers: date-fns: specifier: ^4.4.0 version: 4.4.0 - effect: - specifier: ^4.0.0-rc.112 - version: 4.0.0-rc.112 nanoid: specifier: ^5.1.11 version: 5.1.11 @@ -219,6 +216,9 @@ importers: "@types/validator": specifier: ^13.15.10 version: 13.15.10 + effect: + specifier: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 fast-check: specifier: ^4.9.0 version: 4.9.0 @@ -330,9 +330,6 @@ importers: packages/infra: dependencies: - "@effect/vitest": - specifier: ^4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))) "@faker-js/faker": specifier: ^8.4.1 version: 8.4.1 @@ -345,9 +342,6 @@ importers: "@sentry/node": specifier: 10.55.0 version: 10.55.0(supports-color@8.1.1) - effect: - specifier: ^4.0.0-rc.112 - version: 4.0.0-rc.112 effect-app: specifier: workspace:* version: link:../effect-app @@ -374,8 +368,11 @@ importers: specifier: ^7.9.5 version: 7.9.5(supports-color@8.1.1) "@effect/sql-sqlite-node": - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112) + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/sql-sqlite-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/sql-sqlite-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70) + "@effect/vitest": + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))) "@sentry/opentelemetry": specifier: 10.55.0 version: 10.55.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) @@ -397,6 +394,9 @@ importers: better-sqlite3: specifier: ^12.10.0 version: 12.10.0 + effect: + specifier: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 jwt-decode: specifier: ^4.0.0 version: 4.0.0 @@ -419,12 +419,6 @@ importers: packages/vue: dependencies: - "@effect/atom-vue": - specifier: ^4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e))) - "@effect/platform-browser": - specifier: ^4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112) "@formatjs/intl": specifier: ^4.1.12 version: 4.1.12 @@ -440,9 +434,6 @@ importers: change-case: specifier: ^5.4.4 version: 5.4.4 - effect: - specifier: ^4.0.0-rc.112 - version: 4.0.0-rc.112 effect-app: specifier: workspace:* version: link:../effect-app @@ -453,9 +444,15 @@ importers: specifier: ^3.5.35 version: 3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e)) devDependencies: + "@effect/atom-vue": + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/atom-vue@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/atom-vue@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e))) + "@effect/platform-browser": + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/platform-browser@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/platform-browser@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70) "@effect/vitest": - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))) + specifier: https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))) "@formatjs/icu-messageformat-parser": specifier: ^3.5.10 version: 3.5.10 @@ -465,6 +462,9 @@ importers: "@vitejs/plugin-vue": specifier: ^6.0.7 version: 6.0.7(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e))) + effect: + specifier: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 intl-messageformat: specifier: ^11.2.7 version: 11.2.7 @@ -493,9 +493,6 @@ importers: "@tanstack/vue-form": specifier: ^1.32.0 version: 1.32.0(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e))) - effect: - specifier: ^4.0.0-rc.112 - version: 4.0.0-rc.112 effect-app: specifier: workspace:* version: link:../effect-app @@ -548,6 +545,9 @@ importers: "@vueuse/core": specifier: ^14.3.0 version: 14.3.0(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e))) + effect: + specifier: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 + version: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 jsdom: specifier: ^29.1.1 version: 29.1.1 @@ -1188,10 +1188,12 @@ packages: } hasBin: true - "@effect/atom-vue@4.0.0-rc.112": + "@effect/atom-vue@https://pkg.pr.new/Effect-TS/effect/@effect/atom-vue@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70": resolution: { - integrity: sha512-2sOESDblJGtJMxObqAw9UHHVrqhLHr2ha7JqvXdt2yhf4h57rK+rY3eEjclSgNsshABdcClP/Z0jNvgUD+A42g==, + integrity: sha512-hygZTEoDFGQ5q2+ao3m1yQemfh8CTxAOS/ylRMKkbuFXmClRC91oCt5i/8kvH/a4dXdRUdHmVhkJ8QD8N2cDXA==, + tarball: https://pkg.pr.new/Effect-TS/effect/@effect/atom-vue@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70, } + version: 4.0.0-rc.112 peerDependencies: effect: ^4.0.0-rc.112 vue: ^3.5.35 @@ -1202,10 +1204,12 @@ packages: } hasBin: true - "@effect/platform-browser@4.0.0-rc.112": + "@effect/platform-browser@https://pkg.pr.new/Effect-TS/effect/@effect/platform-browser@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70": resolution: { - integrity: sha512-GSlqNDnjILz2EqOFPhVdMEHxlPq6SGb8+KOpNnLfRvVJjXRTMawEO+v1PCuwt2CHAqESbJAa2Nk+qcQvTrv4MQ==, + integrity: sha512-/6H2ldq7LKktAbk34w2UUOfqlD9M2wCRWvm/O0QzkInjOVGPqAE52qc1MoLD8kTIDV+ZcuDeqM9Xtq5ajywWlw==, + tarball: https://pkg.pr.new/Effect-TS/effect/@effect/platform-browser@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70, } + version: 4.0.0-rc.112 peerDependencies: effect: ^4.0.0-rc.112 @@ -1217,10 +1221,12 @@ packages: peerDependencies: effect: ^4.0.0-beta.86 - "@effect/platform-node-shared@4.0.0-rc.112": + "@effect/platform-node-shared@https://pkg.pr.new/Effect-TS/effect/@effect/platform-node-shared@addeaea": resolution: { - integrity: sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ==, + integrity: sha512-XlXy4u1F/UE7JviJ2t17P7RB3uzxKEwPa6nXwWywE98gBQDcKEybg4rTb7E9QET2IgHyrVcbnwkf6deb4Mmuyw==, + tarball: https://pkg.pr.new/Effect-TS/effect/@effect/platform-node-shared@addeaea, } + version: 4.0.0-rc.112 engines: { node: ">=18.0.0" } peerDependencies: effect: ^4.0.0-rc.112 @@ -1234,19 +1240,23 @@ packages: effect: ^4.0.0-beta.86 ioredis: ^5.7.0 - "@effect/platform-node@4.0.0-rc.112": + "@effect/platform-node@https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70": resolution: { - integrity: sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ==, + integrity: sha512-cTGBpi7ryIFc06wVrEGQmPGyrW5kC2DgaSe3yuPBm7YYlZENVS8uuFTuRoFTgkgU8DWHPz2RrWs03frJLUIqVQ==, + tarball: https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70, } + version: 4.0.0-rc.112 engines: { node: ">=18.0.0" } peerDependencies: effect: ^4.0.0-rc.112 redis: ">=5.0.0 <7.0.0" - "@effect/sql-sqlite-node@4.0.0-rc.112": + "@effect/sql-sqlite-node@https://pkg.pr.new/Effect-TS/effect/@effect/sql-sqlite-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70": resolution: { - integrity: sha512-jGRtbJsn5z7DqsMEPSbXq2Q9mYEvMy1v9DTeziTtDEnDzlxhxfrXkSeupZbYqAP5pukIM1JBa10zfAn6KnHG2Q==, + integrity: sha512-XGbD0nN4fMe/LlklpLBT6tl/fR9oItVQ5MeFu1usTIuX9vKl3PhKrxbmdBCLsXcaivlgXEb9FpCzsyvnQiB8Dw==, + tarball: https://pkg.pr.new/Effect-TS/effect/@effect/sql-sqlite-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70, } + version: 4.0.0-rc.112 peerDependencies: effect: ^4.0.0-rc.112 @@ -1305,13 +1315,15 @@ packages: } hasBin: true - "@effect/vitest@4.0.0-rc.112": + "@effect/vitest@https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70": resolution: { - integrity: sha512-mEKh/FI64mt8JK1/v9mpOrJYdnp+UFZdRUBMEdZMiKz7klg6NPqVgg/oeAGH6wOOQc2iAPcfc2H9BbAv1KyzMQ==, + integrity: sha512-gGXcxkIcb/PpRaAei3oKZfQDOyy35szoM5kD7MEnK0hZlPUOjq6rGFol0C5qd9e6hr37m5HxC8ePmC8xpx57ZA==, + tarball: https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70, } + version: 4.0.0-rc.112 peerDependencies: effect: ^4.0.0-rc.112 - vitest: ">=4.1.0 <5.0.0" + vitest: ">=5.0.0 <6.0.0" "@emnapi/core@1.10.0": resolution: { @@ -5739,10 +5751,12 @@ packages: integrity: sha512-+Y16hy3LhAju/FM3dz3Xp+aGi8dw9AtEzswcvwxREpeEPq+rL8TFS1PgWAO1+/3kKKH4bq9NdBGtfhHzM2K7Ow==, } - effect@4.0.0-rc.112: + effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70: resolution: { - integrity: sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A==, + integrity: sha512-FlnhpW5G20GdwEaH8fSnZF/gvi6BFIeMdz6hjRmQBqwbz2WlrY9qSTuOCQMZHytGPHkiHiQm+GPBBB1cBgWe0Q==, + tarball: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70, } + version: 4.0.0-rc.112 electron-to-chromium@1.5.267: resolution: { @@ -7495,11 +7509,6 @@ packages: integrity: sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA==, } - msgpackr@2.0.5: - resolution: { - integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==, - } - muggle-string@0.4.1: resolution: { integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==, @@ -9114,9 +9123,9 @@ packages: } engines: { node: ">=20.18.1" } - undici@8.10.0: + undici@8.10.2: resolution: { - integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==, + integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==, } engines: { node: ">=22.19.0" } @@ -10324,16 +10333,16 @@ snapshots: - ioredis - utf-8-validate - "@effect/atom-vue@4.0.0-rc.112(effect@4.0.0-rc.112)(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e)))": + "@effect/atom-vue@https://pkg.pr.new/Effect-TS/effect/@effect/atom-vue@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(vue@3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e)))": dependencies: - effect: 4.0.0-rc.112 + effect: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 vue: 3.5.35(typescript@6.0.3(patch_hash=6a20b8df080eb8a9c34008e1802ecfd839f4e90e07f1ee97f78694dbd95a520e)) "@effect/language-service@0.86.2": {} - "@effect/platform-browser@4.0.0-rc.112(effect@4.0.0-rc.112)": + "@effect/platform-browser@https://pkg.pr.new/Effect-TS/effect/@effect/platform-browser@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)": dependencies: - effect: 4.0.0-rc.112 + effect: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 "@effect/platform-node-shared@4.0.0-beta.86(effect@4.0.0-beta.86)": dependencies: @@ -10344,10 +10353,10 @@ snapshots: - bufferutil - utf-8-validate - "@effect/platform-node-shared@4.0.0-rc.112(effect@4.0.0-rc.112)": + "@effect/platform-node-shared@https://pkg.pr.new/Effect-TS/effect/@effect/platform-node-shared@addeaea(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)": dependencies: "@types/ws": 8.18.1 - effect: 4.0.0-rc.112 + effect: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 ws: 8.21.3 transitivePeerDependencies: - bufferutil @@ -10364,20 +10373,19 @@ snapshots: - bufferutil - utf-8-validate - "@effect/platform-node@4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1))": + "@effect/platform-node@https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(redis@6.2.1(@opentelemetry/api@1.9.1))": dependencies: - "@effect/platform-node-shared": 4.0.0-rc.112(effect@4.0.0-rc.112) - effect: 4.0.0-rc.112 - mime: 4.1.0 + "@effect/platform-node-shared": https://pkg.pr.new/Effect-TS/effect/@effect/platform-node-shared@addeaea(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70) + effect: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 redis: 6.2.1(@opentelemetry/api@1.9.1) - undici: 8.10.0 + undici: 8.10.2 transitivePeerDependencies: - bufferutil - utf-8-validate - "@effect/sql-sqlite-node@4.0.0-rc.112(effect@4.0.0-rc.112)": + "@effect/sql-sqlite-node@https://pkg.pr.new/Effect-TS/effect/@effect/sql-sqlite-node@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)": dependencies: - effect: 4.0.0-rc.112 + effect: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 "@effect/tsgo-darwin-arm64@0.31.0": optional: true @@ -10410,9 +10418,9 @@ snapshots: "@effect/tsgo-win32-arm64": 0.31.0 "@effect/tsgo-win32-x64": 0.31.0 - "@effect/vitest@4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0)))": + "@effect/vitest@https://pkg.pr.new/Effect-TS/effect/@effect/vitest@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70(effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0)))": dependencies: - effect: 4.0.0-rc.112 + effect: https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70 vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.15(@types/node@25.9.1)(esbuild@0.28.0)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0)) "@emnapi/core@1.10.0": @@ -13058,10 +13066,7 @@ snapshots: uuid: 14.0.0 yaml: 2.9.0 - effect@4.0.0-rc.112: - dependencies: - fast-check: 4.9.0 - msgpackr: 2.0.5 + effect@https://pkg.pr.new/Effect-TS/effect/effect@addeaea0b0abe0dc24ca1d4d0fd194a0e458be70: {} electron-to-chromium@1.5.267: optional: true @@ -14387,10 +14392,6 @@ snapshots: optionalDependencies: msgpackr-extract: 3.0.4 - msgpackr@2.0.5: - optionalDependencies: - msgpackr-extract: 3.0.4 - muggle-string@0.4.1: {} multipasta@0.2.7: {} @@ -15701,7 +15702,7 @@ snapshots: undici@7.25.0: {} - undici@8.10.0: {} + undici@8.10.2: {} undici@8.3.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index af34629485..2df6c35016 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,7 @@ packages: - packages/* +# pkg.pr.new Effect tarballs nest URL deps; required to consume unpublished RC main. +blockExoticSubdeps: false allowBuilds: "@parcel/watcher": false better-sqlite3: true diff --git a/repos/effect/.agents/AGENTS.md b/repos/effect/.agents/AGENTS.md index c7bb67c037..1f33cf814c 100644 --- a/repos/effect/.agents/AGENTS.md +++ b/repos/effect/.agents/AGENTS.md @@ -1,163 +1,46 @@ -This is the Effect library repository, focusing on functional programming patterns and effect systems in TypeScript. +This is the Effect TypeScript monorepo. The git base branch is `main`; use `pnpm` from the repository root. -## Overview +## Layout -- 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. +- Core library source, runtime tests, and type tests are in `packages/effect/src`, `packages/effect/test`, and + `packages/effect/typetest`. +- Other package families include `packages/ai`, `packages/atom`, `packages/platform`, `packages/sql`, and + `packages/tools`; standalone packages also live directly under `packages`. +- Package tests and type tests live beside source in `test` and `typetest` directories. +- AI documentation sources are in `ai-docs/src`. +- Changesets are in `.changeset`. +- Migration sources (v3-to-v4) are in `migration/annotations`. +- Inspect nearby code before editing. ## Validation -Use the narrowest validation that still covers the change: +Use the narrowest validation that covers the change: | Change type | Validation | | -------------------------------- | ---------------------------------------------------------------------------------- | | Code changes | `pnpm lint-fix`, targeted `pnpm test --run `, `pnpm check` | | Tests-only changes | `pnpm lint-fix`, targeted `pnpm test --run `, `pnpm check` | | Type-level/API type changes | Targeted `pnpm test-types `, plus `pnpm check` when source types changed | -| JSDoc text/category/link changes | `pnpm lint` | -| JSDoc example changes | `pnpm lint`; root `pnpm doctest --run ` | -| Docs-only changes | `pnpm lint-fix`; no tests required unless examples or code changed | - -Never run the whole test suite. A bare `pnpm test` or `pnpm doctest` runs every package in watch mode and will not -exit; always pass `--run` and the specific test files covering your change. CI runs the full suite -on push, so leave that to CI. - -## Bundle Size Preview - -When asked to show bundle-size impact for a commit, use the existing bundle comparison workflow: - -1. For the latest commit, run `pnpm bundle-compare HEAD~1`. - For another base, run `pnpm bundle-compare `. -2. Read the Markdown report from `tmp/bundle-stats.txt` and summarize the non-zero differences. -3. Leave `tmp/bundle-base` in place unless cleanup is requested. To clean it up, run `git worktree remove --force tmp/bundle-base`. +| JSDoc text/category/link changes | `pnpm jsdocs --check`, `pnpm lint` | +| JSDoc example changes | `pnpm jsdocs --check`, `pnpm lint`, root `pnpm doctest --run ` | +| Docs-only changes | `pnpm lint-fix`; no tests unless examples or code changed | -## Coding Patterns +Never run bare `pnpm test` or `pnpm doctest`; both start the full suite in watch mode. Always pass `--run` and the +specific files covering the change. CI runs the full suite. -Read `.patterns/effect.md` before changing Effect code. In particular: +For an ad hoc runnable probe, create `scratchpad/.ts`, run it with plain `node`, and remove it when finished. -- Prefer `Effect.fnUntraced` over functions that only return `Effect.gen`. -- Prefer class syntax for `Context.Service`. -- Do not use `async` / `await` or `try` / `catch`; use Effect APIs such as `Effect.gen`, `Effect.fnUntraced`, and `Effect.tryPromise`. -- Do not use `Date.now` or `new Date`; use `Clock`, and use `TestClock` in tests. - -## Testing - -Read `.patterns/testing.md` before writing or changing tests. - -- Run only the tests covering the files you changed. -- From the repository root, run an affected package with `pnpm --filter effect test --run` only when package-wide coverage is necessary. -- Prefer a single test file, using a path relative to the package: `pnpm --filter effect test --run test/Option.test.ts`. - Replace the package name and test path with those covering your changed files, and narrow further with `-t ""` when useful. -- Test files are located in `packages/*/test/`. -- Main Effect library tests are in `packages/effect/test/`. -- Use `it.effect` for Effect-returning tests. -- `it.effect` and `it.live` already provide and close a `Scope` for each test; do not wrap test bodies in `Effect.scoped`. -- Use regular `it` for pure synchronous tests. -- Do not use `Effect.runSync` in tests. -- Do not use `expect` from Vitest; use `assert` from `@effect/vitest`. -- Type-level tests are in `packages/*/typetest/` and run with `pnpm test-types `. - -## Documentation - -- For AI documentation, read `ai-docs/README.md` very carefully before writing examples. -- AI documentation changes may include explanatory comments when useful. -- For public JSDoc categories and example best practices, read `.patterns/jsdoc.md`. -- Mark runnable TypeScript examples with `````ts import.meta.vitest``. Leave examples that register Vitest tests or suites - as plain `````ts`` fences because the doctest collector executes runnable snippets inside tests; invoke registration - APIs directly to show their intended top-level usage. -- Prefer direct trailing value assertions such as `operation() // => Option.some(1)`. Keep bindings only for reuse or meaningful multi-step setup, separate later assertion blocks with a blank line, use dense expected arrays such as `[1, 2]`, and keep a call on one line when the complete line is at most 120 characters. -- Assert semantic values rather than console formatting. Preserve `import.meta.vitest` on type-level examples without adding tautological runtime assertions. -- Keep marked examples self-contained, deterministic, bounded, and free of external-service dependencies. Await asynchronous work. -- Run `pnpm doctest --run ` from the repository root to execute changed examples. +Report any commands that could not be run. ## 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 ---- +Do not edit generated output directly. -A description of the change. -``` +Some `index.ts` sections marked with `@barrel` are generated. Do not edit those +sections manually; update their source modules and run `pnpm codegen`. +Hand-maintained `index.ts` files and unmarked sections are not covered by this +rule. -Tests-only changes, internal refactors, docs-only changes, and JSDoc-only maintenance may skip changesets by maintainer decision. +`LLMS.md` is generated from `ai-docs/src`, and `migration/v3-to-v4.md` is +generated from `migration/annotations`. Update checked-in third-party assets +through their generator or documented import procedure. diff --git a/repos/effect/.agents/skills/ai-docs/SKILL.md b/repos/effect/.agents/skills/ai-docs/SKILL.md new file mode 100644 index 0000000000..02e6757330 --- /dev/null +++ b/repos/effect/.agents/skills/ai-docs/SKILL.md @@ -0,0 +1,16 @@ +--- +name: ai-docs +description: AI documentation. Use when editing ai-docs/src or regenerating LLMS.md. +--- + +Read `ai-docs/README.md`; it owns source structure, style, examples, generation, +and changeset policy. + +1. Inspect neighboring source examples and the relevant current `LLMS.md`. +2. Edit sources under `ai-docs/src`, then run `pnpm ai-docgen`. +3. Review generated output for ordering, missing content, and unrelated changes. +4. Run applicable root validation. + +The task is complete when sources and generated output agree, every generated +difference is explained, TypeScript checks pass when examples or fixtures +change, and applicable checks pass or are reported as not runnable. diff --git a/repos/effect/.agents/skills/bundle-analysis/SKILL.md b/repos/effect/.agents/skills/bundle-analysis/SKILL.md new file mode 100644 index 0000000000..28d88976e1 --- /dev/null +++ b/repos/effect/.agents/skills/bundle-analysis/SKILL.md @@ -0,0 +1,25 @@ +--- +name: bundle-analysis +description: Bundle analysis. Use when measuring current bundle size, comparing stable or selected fixtures, inspecting composition, or cleaning retained comparison state. +--- + +Read `packages/tools/bundle/README.md`, then select one workflow: + +- **Stable comparison:** `pnpm bundle-compare `. Use `HEAD~1` for the + latest commit. Read `tmp/bundle-stats.txt` and report non-zero differences. +- **Selected comparison:** `pnpm bundle-compare-selected --base + scratchpad/.ts`. Use only user-named or investigation-local fixtures; + keep temporary fixtures out of the stable corpus. +- **Composition:** `pnpm bundle-analyze scratchpad/.ts`. Read raw data + first and report the largest modules, dependency groups, and surprising + inclusions. Its readable-name output is not an exact size measurement. +- **Current size:** Build immediately before the direct bundle report so Effect + packages resolve from current `dist` output. +- **Cleanup:** Stable comparisons retain `tmp/bundle-base` for reuse unless + cleanup is requested. Remove selected-comparison state retained with + `--keep-base` after its final use. Verify cleanup without disturbing unrelated + worktrees. + +The task is complete when the selected artifact is inspected, requested size or +composition findings are reported, generated paths are named, and comparison +state follows the selected workflow's retention policy. diff --git a/repos/effect/.agents/skills/changesets/SKILL.md b/repos/effect/.agents/skills/changesets/SKILL.md new file mode 100644 index 0000000000..0f4186cfc2 --- /dev/null +++ b/repos/effect/.agents/skills/changesets/SKILL.md @@ -0,0 +1,58 @@ +--- +name: changesets +description: Changesets. Use after consumer-visible runtime, public API, entrypoint, lifecycle, or wire-format changes, when deciding whether a change is breaking, or when authoring changesets and consumer release notes. +--- + +Record what consumers need to know after implementation and focused validation. + +## Workflow + +1. Inspect the complete diff and identify directly affected published packages. +2. Classify impact across source types, runtime behavior, entrypoints, required + services, lifecycle, and persisted or wire data. +3. Perform the breaking audit below. +4. Record either a reason no changeset is required or one coherent changeset. + When required, read [authoring.md](authoring.md). +5. Validate package names, frontmatter, bump policy, and consumer-facing text. + +Do not include unrelated worktree changes. + +## Requirement + +Create a changeset for observable runtime behavior changes, including bug +fixes; exported value or public type changes; entrypoint or export-map changes; +changes to required services, errors, ownership, defaults, or lifecycle; and +persisted, serialization, protocol, or wire-format changes. + +Tests-only changes, behavior-preserving internal refactors, documentation or +JSDoc maintenance, and unpublished tooling normally do not need one. When +unclear, inspect exports and consumer-visible declarations rather than inferring +from source location. + +## Breaking Audit + +A change is breaking when valid existing consumer code, configuration, or data +must change to keep compiling or behaving according to the previous contract. +Audit every surface: + +- **Names and locations:** exports, entrypoints, compatibility exports, and + module paths. +- **Call compatibility:** parameters, accepted inputs, overload resolution, + generic parameters, and defaults. +- **Result compatibility:** return and error types, output narrowing, members, + inference, and required services. +- **Runtime contracts:** defaults, failures, interruption, concurrency, + ordering, resource lifetime, acquisition, cleanup, and mutation. +- **Data compatibility:** persisted schemas, encodings, database layouts, + protocols, and wire formats. + +Additive exports, optional parameters, and behavior-preserving implementations +are normally non-breaking. A fix restoring the documented contract is normally +non-breaking but still needs a changeset when its operational impact is +consumer-visible. Verify representative existing calls when overload ordering, +structural assignability, or inference makes compatibility uncertain. API diff +output is mechanical evidence, not a semantic-version decision. + +The task is complete when every affected surface and published package is +accounted for and either the no-changeset decision is explicit or one coherent, +valid changeset describes the consumer impact and migration for every break. diff --git a/repos/effect/.agents/skills/changesets/authoring.md b/repos/effect/.agents/skills/changesets/authoring.md new file mode 100644 index 0000000000..cb8fcb99c6 --- /dev/null +++ b/repos/effect/.agents/skills/changesets/authoring.md @@ -0,0 +1,33 @@ +# Authoring Changesets + +Create one `.changeset/.md` per coherent change: + +```md +--- +"effect": patch +"@effect/affected-package": patch +--- + +Describe the consumer-visible change and why it matters. +``` + +List every directly affected published package. Do not list packages merely +because they share the fixed release group in `.changeset/config.json`. + +Choose the bump from current release policy: + +- On a stable line, use `patch` for compatible fixes, `minor` for compatible + additions, and `major` for breaks. +- In `.changeset/pre.json` `rc` mode, follow the current convention of recording + v4 release-candidate changes, including breaking cleanups, as `patch` unless a + maintainer requests another level. +- Ask when release mode or intent is ambiguous. + +Write for consumers. State the changed behavior or API and give concrete +migration guidance for every break. Use a `### Breaking changes` section when +several breaks need separate scanning. Include before/after examples only when +they materially clarify migration. Omit implementation and test details. + +Validate frontmatter against published package names and inspect nearby current +changesets for wording and release convention. Never run `changeset-version` or +`changeset-publish` as contributor validation. diff --git a/repos/effect/.agents/skills/ci-maintenance/SKILL.md b/repos/effect/.agents/skills/ci-maintenance/SKILL.md new file mode 100644 index 0000000000..bfabe0b4d9 --- /dev/null +++ b/repos/effect/.agents/skills/ci-maintenance/SKILL.md @@ -0,0 +1,48 @@ +--- +name: ci-maintenance +description: GitHub Actions maintenance. Use when authoring or reviewing workflows or composite actions, especially event, permission, action-reference, artifact, setup, or concurrency changes. +--- + +Audit the trust boundary before editing. This skill owns Actions event +semantics, workflow security, permissions, action pinning, setup reuse, +untrusted inputs, timeouts, concurrency, and artifact trust boundaries. + +## Workflow + +1. Decide whether this is a review or an implementation. Reviews produce + evidence-backed findings without editing; implementations continue through + the smallest boundary-preserving change. +2. Inspect every affected workflow, composite action, and caller. List all + affected jobs and actions before proceeding. +3. For each job, record the trigger, actor/fork status, workflow revision, + checked-out revision, permissions, credentials, and every external input and + consumer. Include artifacts, caches, outputs, PR fields, refs, dispatch + inputs, generated files, and called actions. +4. Consult current GitHub documentation for every platform-sensitive decision. + Repository examples establish local convention, not platform semantics. +5. Apply every applicable rule: + - Default workflow permissions to empty and grant minimal job permissions. + - Reuse the repository's shared setup action unless the environment must differ. + - Give executable jobs intentional matrices and realistic timeouts. + - Pin external actions and reusable workflows to full commit SHAs with + readable release comments; verify each SHA belongs to that upstream release. + - Analyze event choice, checkout revision, and credential persistence + together. + - Pass untrusted expressions through environment variables; validate and + quote them before use. + - Derive concurrency from interruption safety. +6. For forks, privileged credentials, publication, or cross-run data, read + [privileged-workflows.md](privileged-workflows.md). +7. Run the narrowest syntax and repository checks, inspect the complete diff, + and identify behavior testable only on GitHub-hosted runners. + +For local patterns, inspect the current shared setup action and the nearest +workflow with the same trust boundary. Use ordinary check workflows for routine +precedent and privileged publication workflows only for equivalent trust +boundaries. + +The task is complete when every affected job and caller has a recorded trust +boundary, every applicable rule has evidence or a compensating control, local +checks pass, and GitHub-only verification is identified. Reviews report every +failure with evidence; implementations resolve every failure and contain only +intended behavior. diff --git a/repos/effect/.agents/skills/ci-maintenance/privileged-workflows.md b/repos/effect/.agents/skills/ci-maintenance/privileged-workflows.md new file mode 100644 index 0000000000..3cc61cd8f8 --- /dev/null +++ b/repos/effect/.agents/skills/ci-maintenance/privileged-workflows.md @@ -0,0 +1,20 @@ +# Privileged Workflows + +- Privileged jobs must not run untrusted repository code with secrets or write + credentials. +- Validate cross-run data for expected source, identity, type, shape, and + bounded size before using it in commands, outputs, APIs, or comments. +- Require the intended approval boundary before fork-originated work reaches + privileged execution. +- Preserve publication and other irreversible operations until completion; + cancel only replaceable checks and previews. +- Make checkout revision and credential persistence explicit. Account for + every write permission and credential. + +Search current workflows by event, job purpose, action, and permission to find +the nearest precedent for fork approval gates, publication, non-cancelable +concurrency, and validation of cross-run artifacts before privileged use. + +Consult current GitHub documentation for event, token, secret, permission, +checkout, artifact, reusable-workflow, and environment semantics. Start with +the workflow events, workflow syntax, and secure-use references. diff --git a/repos/effect/.agents/skills/dependency-maintenance/SKILL.md b/repos/effect/.agents/skills/dependency-maintenance/SKILL.md new file mode 100644 index 0000000000..5a409b3ad3 --- /dev/null +++ b/repos/effect/.agents/skills/dependency-maintenance/SKILL.md @@ -0,0 +1,58 @@ +--- +name: dependency-maintenance +description: Dependency maintenance. Use when adding, moving, or upgrading dependencies, changing pnpm or JavaScript runtimes, updating TypeScript or build/test tools, or changing native-build policy, patches, or test images. +--- + +Treat an upgrade as a synchronization task, not a lockfile refresh. Derive +versions, commands, and compatibility points from the current repository. + +## Discover + +1. Read affected manifests and scripts, `pnpm-workspace.yaml`, setup actions, + workflows, test configuration, and compatibility documentation. +2. Search for the dependency and every current version, range, runtime input, + image, engine constraint, patch, and compatibility claim. +3. Record each match as a development version, tested version, peer range, + engine minimum, advertised minimum, or intentionally different constraint. +4. When adding or moving a manifest entry, read + [manifest-roles.md](manifest-roles.md) before selecting its role. +5. Select the package-local or coordinated branch and its validation matrix. + +Discovery is complete when every match and affected validation surface is +accounted for. + +## Package-local branch + +Use this branch only for one dependency in one workspace package when runtime, +compiler, package-manager, image, patch, native-build, and shared-tooling policy +are unchanged. + +Update only the owning manifest and keep peer compatibility independent from +the development version tested here. If any coordinated surface appears, +switch branches. + +This branch is complete when the manifest and lockfile agree, focused checks +pass, and every search result is intentionally unchanged or package-local. + +## Coordinated branch + +Read [coordinated-upgrades.md](coordinated-upgrades.md), select every applicable +row, and update all listed synchronization points before installing. Apply root +workflow and generated-file requirements before editing those surfaces, then +return here to finish matrix and lockfile review. + +## Install and finish + +Run root `pnpm install` after all selected edits. Inspect warnings and the +semantic lockfile diff for specifiers, resolutions, duplicate transitives, peer +changes, integrity, patch hashes, lifecycle scripts, and `allowBuilds` effects. +A successful install alone does not complete this review. + +Run the narrowest correctness and performance checks that cover every selected +matrix row. +Apply the root changeset routing after implementation and focused validation. + +The task is complete when repeated repository searches find no unclassified +synchronization point, manifest roles and compatibility ranges are intentional, +the lockfile has no unexplained churn, and every selected check passes or is +reported as not runnable. diff --git a/repos/effect/.agents/skills/dependency-maintenance/coordinated-upgrades.md b/repos/effect/.agents/skills/dependency-maintenance/coordinated-upgrades.md new file mode 100644 index 0000000000..8bb028021c --- /dev/null +++ b/repos/effect/.agents/skills/dependency-maintenance/coordinated-upgrades.md @@ -0,0 +1,21 @@ +# Coordinated Upgrades + +Select every applicable row and derive commands from the cited current source. + +| Change | Synchronize | Validation source | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | +| Shared dependency or test/build tool | Owning manifests, peer/development pairs, configs, and generated artifacts owned by that tool | Root/package scripts and affected Vitest projects | +| pnpm | Root `packageManager`, workspace settings, setup/cache assumptions, workflows, and lockfile format | Current install, lint, check, build, and focused test scripts | +| Node, Deno, or Bun | Setup inputs, workflow jobs, engines, runtime metadata, compatibility docs, and runtime config | Current runtime workflow jobs and test configuration | +| TypeScript support or compiler | Compiler/tooling dependencies, `test-types` target, CI target, tool peer ranges, compatibility docs, and typetests | Targeted typetests; typeperf protocol for measured paths | +| Native package or install policy | Owning manifests and `pnpm-workspace.yaml#allowBuilds` | Fresh install plus focused package build and tests | +| Patched dependency | Manifest/range, `patchedDependencies`, patch file, and lockfile patch hash | Fresh install and the behavior that required the patch | +| Container image or Testcontainers package | Workflow pre-pulls, image call sites, manifests, and integration project names | Current integration workflow and Vitest configuration | + +For a patched dependency, test the new release without the patch when feasible. +Remove obsolete registration and patch files. Otherwise refresh the patch using +the current pnpm workflow and verify both that it applies and that the original +patched behavior still requires it. + +When an upgraded component lies on a measured path, use the repository's +runtimeperf or typeperf comparison protocol rather than inventing a benchmark. diff --git a/repos/effect/.agents/skills/dependency-maintenance/manifest-roles.md b/repos/effect/.agents/skills/dependency-maintenance/manifest-roles.md new file mode 100644 index 0000000000..0023a32f9f --- /dev/null +++ b/repos/effect/.agents/skills/dependency-maintenance/manifest-roles.md @@ -0,0 +1,15 @@ +# Manifest Roles + +Inspect the nearest package with the same integration shape, then justify each +added or moved entry: + +| Role | Use when | Completion check | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `dependencies` | Published runtime code requires its own installed copy. | A packed consumer receives it without adding an undeclared package. | +| `peerDependencies` | Consumers provide a compatible shared package or the public contract integrates with their copy. | The range states supported consumer versions; add a development entry when local build or tests need an installed copy. | +| `devDependencies` | Only repository build, test, type, benchmark, or code-generation work needs it. | Published runtime code and declarations do not require consumers to install it. | +| `optionalDependencies` | A runtime feature handles absence and installation failure must not block the base package. | Focused tests cover present and absent behavior. | +| Optional peer via `peerDependenciesMeta` | A consumer-provided integration is genuinely optional. | The peer remains in `peerDependencies`, and code requires it only when selected. | + +Role selection is complete when every changed entry has one justified role and +peer compatibility remains independent from repository validation versions. diff --git a/repos/effect/.agents/skills/effect-development/SKILL.md b/repos/effect/.agents/skills/effect-development/SKILL.md new file mode 100644 index 0000000000..079f8e08db --- /dev/null +++ b/repos/effect/.agents/skills/effect-development/SKILL.md @@ -0,0 +1,16 @@ +--- +name: effect-development +description: Effect development guidance. Use when implementing or reviewing code that composes Effect APIs or patterns. +--- + +Use the repository's AI documentation as targeted reference, not ambient +context: + +1. Identify the Effect APIs and concepts involved in the change. +2. Search `LLMS.md` and `ai-docs/src` for those names and concepts, then read + the matching sections and examples. +3. Apply the relevant guidance and inspect nearby source and tests for any + repository-internal conventions not covered by the documentation. + +The task is ready for implementation when every material Effect API or pattern +has matching local guidance, or the search has established that none exists. diff --git a/repos/effect/.agents/skills/jsdocs/SKILL.md b/repos/effect/.agents/skills/jsdocs/SKILL.md index a2972d8d87..dac9327eed 100644 --- a/repos/effect/.agents/skills/jsdocs/SKILL.md +++ b/repos/effect/.agents/skills/jsdocs/SKILL.md @@ -1,260 +1,33 @@ --- 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. +description: Public API JSDoc. Use when authoring or reviewing Effect API documentation, refining a module's documentation, or fixing documentation related diagnostics. --- -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 import.meta.vitest - * const result = example() - * ``` - * - * @category constructors - * @since 1.0.0 - */ -``` - -## Prose Rules - -- Use sober, practical prose. -- Write all public JSDoc prose in English. -- Do not use jargon when a plain word works. -- Do not be clever. -- Do not add filler sections. -- The short description is required and must be exactly one paragraph. -- Make the short description stand on its own. Do not rely on `**When to use**` - to make the API understandable. -- For functions and methods, prefer present-tense, action-first prose such as - `Creates`, `Returns`, `Checks`, `Provides`, `Represents`, `Converts`, - `Decodes`, or `Formats`. -- For technical value exports, use consistent noun forms such as `Schema for`, - `Layer that`, `Service that`, `Context reference that`, or - `Constructors and matchers for`. -- Avoid leading `A` or `An` for canonical technical nouns when the surrounding - module uses a standard noun family, for example prefer `Schema for ...` over - `A schema for ...`. -- Do not describe implementation mechanics when a public concept is clearer. - For example, prefer `Constructors and matchers for ...` over wording that - only says an API uses `Data.taggedEnum`. -- Avoid generic purity or non-mutation remarks unless they document a real - surprise, caveat, or meaningful contrast with a mutating-looking API. -- Optional sections must appear in this order: - 1. `**When to use**` - 2. `**Details**` - 3. `**Gotchas**` -- Include an optional section only when it has useful, non-empty content. -- Prefer prose over bullet lists for single-item `**Details**`, `**When to use**`, or `**Gotchas**` sections. Use bullets only when there are two or more parallel facts, options, cases, or caveats. -- `**When to use**` describes the positive use case for the documented API. Do not use it as a routing section for sibling APIs. If neighboring APIs need to be mentioned, put that boundary in `@see` tag text instead. -- `**When to use**` is important when the API has close alternatives, trade-offs, or `@see` tags. If `@see` tags are present, inspect the referenced APIs and add `**When to use**` when it clarifies the documented API's own use case. -- `**When to use**` must start with one of these practical guidance forms: `Use to`, `Use when`, `Use as`, or `Use with`. Avoid bullet lists and vague openers such as `Use this...` or `Useful for...`. -- Prefer reader-centered `**When to use**` wording, especially `Use when you ...`, - when the sentence describes a user's goal. Avoid third-person noun-phrase - subjects such as `the input is ...`, `a service needs ...`, or - `values should ...` when they would become awkward in generated prompts. -- A good `**When to use**` sentence should still read naturally if reused as - a user intent prompt, for example after `I need ...` or `I have ...`. -- Keep `short` and `**When to use**` distinct: the short description says what - the API is or does; `**When to use**` says when to choose it. -- Add internal `@see` tags only for semantically useful related public APIs. -- Write `@see` tag text as normal prose after the link; no special separator is required. Prefer forms like `@see {@link otherApi} for ...` when a short explanation helps. -- Use exactly one blank line between the short description, sections, examples, and tags. -- Do not use Markdown headings such as `# Heading` or ad hoc bold headings such as `**Notes**`; only the standard headings are allowed. -- Examples must use `**Example** (Title)`, optional prose, and exactly one non-empty `ts` code fence. -- Example titles must be unique after trimming and lowercasing. -- Example titles should be short use-case phrases, not generic labels. -- Prefer gerund or action-noun titles that read naturally after `for`, for - example `Parsing JSON`, `Creating a scoped runtime`, or `Comparing structs`. -- Avoid imperative titles such as `Parse JSON`, vague labels such as `Syntax` - or `Basic usage`, and title-cased fragments such as `String Ordering`. -- Preserve canonical technical capitalization inside the phrase, such as - `Option`, `Effect`, `Schema`, `DateTime`, `HashMap`, `Base64`, and `JSON`. -- For multiple examples on the same API, make each title describe the distinct - use case shown by that example. -- Prefer examples with stable, deterministic output. Avoid assertions or - `console.log` comments that depend on stack traces, object inspection, - `Error` formatting, concurrency order, timing, randomness, or - environment-specific formatting. Examples may assume Node.js console - formatting. Direct `Set` / `Map` output is acceptable when insertion order is - deterministic and the expected output uses Node's format; otherwise - demonstrate a stable property instead. -- Do not use `@example`. -- Do not put TypeScript code fences outside `**Example** (Title)` sections. -- Inline `{@link Symbol}` targets must resolve to TypeScript symbols; do not link to URLs with `{@link}`. -- Avoid overlinking in prose. Use `{@link Symbol}` only when navigation to - that symbol helps the reader choose or understand the API. For the API being - documented, the module's central type, nearby obvious names, or repeated - mentions, prefer plain code formatting such as `Cause`, `Effect`, or - `Context`. -- Do not document module-level comments; module JSDoc is ignored by this rule. -- `@internal` means the item is ignored; do not rewrite it as public docs. -- Default exports are ignored by this rule and do not need JSDoc. -- Do not add unsupported constructs such as enums or empty exports in checked files. -- For low-level public values, prefer accurate categories such as `symbols`, - `type IDs`, or `prototypes` over compensating with verbose descriptions. - -## Example quality - -Examples are optional. They should demonstrate: - -- behavior or constraints that are not clear from the signature; -- meaningful composition with other public APIs; -- a realistic use case supported by repository tests or call sites; or -- useful type inference, narrowing, or overload behavior. - -A good example: - -- focuses on the documented API and includes only the context needed to - understand it; -- is a complete, self-contained TypeScript module without placeholders or - omitted setup; -- imports public APIs rather than internal modules or unrelated test helpers; -- uses stable, deterministic, bounded behavior and does not require network - access, external services, timing assumptions, randomness, or machine-specific - state; -- demonstrates the meaningful result, with a concise expected-value comment - when useful; and -- uses explanatory prose only when the code cannot communicate an important - choice or caveat on its own. - -### Executable examples - -- Mark runnable TypeScript fences with `import.meta.vitest`. Run changed examples from the repository root with `pnpm doctest --run `. -- Write each marked example as a complete isolated module. Import public APIs, define every runtime value, await asynchronous work, and keep execution deterministic and bounded. -- Prefer `operation() // => expected` over introducing a result binding used only by the assertion. Retain bindings for reuse, mutation, identity checks, or meaningful multi-step setup, and insert a blank line before a separate assertion block. -- Keep direct assertions on one line up to 120 characters. Use dense expected arrays such as `[1, 2]` and semantic Effect values such as `Option.some(1)` rather than console formatting. -- Preserve `import.meta.vitest` for type-level examples, but do not add tautological runtime assertions to them. -- Leave examples that register Vitest tests or suites as plain `````ts`` fences because the doctest collector executes - runnable snippets inside tests. Call the registration API directly to show its intended top-level usage. -- Keep documentation-only snippets as plain `````ts`` fences. - -When reviewing existing examples: - -1. Derive the example's use case and behavior from repository evidence. Inspect - the declaration, implementation, tests, call sites, and related APIs. Do not - invent a scenario merely to retain an example. -2. Keep a correct, clear, high-value example without gratuitous rewriting. -3. Fix or replace an example when repository evidence supports a concise, - valuable version. -4. Remove an example when it is trivial, misleading, contrived, or requires more - scaffolding than the insight justifies. Also remove it when a good replacement - would require guessing at a use case. - -Prefer concise trailing `// =>` assertions that keep the meaningful result visible; -public documentation should not look like a test suite. Type-level examples may demonstrate inference or assignability -without runtime assertions. For lazy APIs such as `Effect`, execute enough of the -program to demonstrate the behavior unless the example's value is specifically -type-level or construction-oriented. - -If an example review exposes a likely implementation or type-definition bug, -do not change runtime or API code as part of the documentation pass. Report the -finding and do not present the suspected behavior as recommended usage. - -## Tag rules - -When multiple tags are present, keep them in this order: - -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 runnable JSDoc example changes, run `pnpm doctest --run ` from the repository root. -- Run `pnpm lint` because the linter includes the custom rule that checks public API JSDoc. -- Do not run broad validation for prose-only skill edits. +1. Inspect the declaration, implementation, nearby JSDoc, tests, and call sites. +2. Select every applicable branch and load its reference before editing: + - tags, modules, or links: [declarations.md](declarations.md); + - categories: [categories.md](categories.md); + - examples: [examples.md](examples.md). +3. Make a focused API fix or module refinement. Preserve verified facts and + valuable examples rather than rewriting mechanically. +4. For module refinement or APIs with close alternatives, audit `@see` links + and inspect implementation and tests for concrete `**Gotchas**`. +5. Run `pnpm jsdocs --check` and every applicable root validation command. + +`@internal` declarations and default exports are outside public JSDoc +authoring. Checked files do not support exported enums or empty export +declarations. + +Public declarations use a multiline block with one self-contained practical +description paragraph. Start functions and methods with a present-tense action; +match nearby noun-family phrasing for values. Optional non-empty sections appear +once in this order: `**When to use**`, `**Details**`, `**Gotchas**`. Separate +descriptions, sections, examples, and tags with one blank line. `**When to +use**` states a positive use case distinct from the description; reserve +`**Gotchas**` for concrete caveats and failure modes. + +The task is complete when every changed public declaration satisfies each +applicable reference, the checker passes, runnable examples pass their targeted +doctest, and all other applicable root checks pass or are reported as not run. diff --git a/repos/effect/.agents/skills/jsdocs/categories.md b/repos/effect/.agents/skills/jsdocs/categories.md new file mode 100644 index 0000000000..1b0817e575 --- /dev/null +++ b/repos/effect/.agents/skills/jsdocs/categories.md @@ -0,0 +1,24 @@ +# Categories + +Root declarations require one non-empty `@category`. Reuse nearby categories; +prefer lowercase plurals and gerunds while preserving canonical domain casing. + +Common categories include: + +- Shapes: `constructors`, `destructors`, `models`, `schemas`, `guards`, + `predicates`, `getters`, `accessors`, `instances`, `constants`, `protocols`, + `prototypes`, `re-exports`, `unsafe`, `testing`. +- Effect: `services`, `tags`, `layers`, `context`, `resource management`, + `running`, `errors`, `error handling`. +- Operations: `combinators`, `filtering`, `mapping`, `sequencing`, `zipping`, + `combining`, `merging`, `converting`, `transforming`, `folding`, `splitting`, + `repetition`. +- Shared: `utility types`, `encoding`, `decoding`, `serialization`, `tracing`, + `metrics`, `logging`, `annotations`, `references`, `symbols`, `type IDs`, + `configuration`, `math`, `comparisons`, `ordering`. + +Keep these boundaries: services are contracts, tags identify services, and +layers provide them; getters retrieve values while accessors read context; +errors model failures while error handling recovers or maps them; models are +domain data while utility types are type-level contracts; guards narrow while +predicates return booleans. diff --git a/repos/effect/.agents/skills/jsdocs/declarations.md b/repos/effect/.agents/skills/jsdocs/declarations.md new file mode 100644 index 0000000000..62937bd366 --- /dev/null +++ b/repos/effect/.agents/skills/jsdocs/declarations.md @@ -0,0 +1,87 @@ +# Tags, Modules, And Links + +## Declaration Shape + +Use a multiline JSDoc block for a public declaration: + +````ts +/** + * Short description as one paragraph. + * + * **When to use** + * + * Optional practical usage guidance. + * + * **Details** + * + * Optional details for complex behavior. + * + * **Gotchas** + * + * Optional concrete caveats. + * + * **Example** (Parsing JSON) + * + * ```ts import.meta.vitest + * operation() // => expected + * ``` + * + * @category constructors + * @since 1.0.0 + */ +```` + +Write practical English about the public concept, not its implementation. Start +functions and methods with a present-tense action such as `Creates`, `Returns`, +or `Converts`; match nearby noun families for values, such as `Schema for`, +`Layer that`, or `Service that`. + +Optional non-empty sections appear once in this order: `**When to use**`, +`**Details**`, `**Gotchas**`. Separate descriptions, sections, examples, and +tags with exactly one blank line. Use prose for one fact and bullets for +parallel facts. Do not add other headings. + +`**When to use**` states a positive use case distinct from the description and +begins with `Use to`, `Use when`, `Use as`, or `Use with`. Put sibling +comparisons in `@see`. Reserve `**Gotchas**` for concrete preconditions, edge +cases, surprising behavior, and important failure modes. + +## Tags + +Declaration tags appear in this order: + +1. `@deprecated` +2. `@default` +3. `@see` +4. `@category` +5. `@since` + +- Roots require stable-semver `@since` and no `@default`; category requirements + live in [categories.md](categories.md). +- Namespaces and their declarations require stable-semver `@since`, permit + `@category`, and reject `@default`. +- Member JSDoc is optional; when present it permits stable-semver `@since` and + non-empty `@default`, rejects `@category`, and follows the prose contract. +- Any declaration permits one non-empty `@deprecated` and repeated non-empty + `@see` tags. + +Use canonical `**Example**` sections rather than `@example` tags or loose code +fences. + +## Modules And Links + +When present, the first top-level JSDoc is the module block unless TypeScript +attaches it to a non-import first declaration. An `@internal` module is omitted. +Module prose does not use the declaration template. Its tags are optional +non-empty `@deprecated`, repeated non-empty `@see`, then required stable-semver +`@since`. Its examples and links follow the declaration contracts. + +Inline `{@link Symbol}` targets must resolve to TypeScript symbols; use normal +Markdown links for URLs. Prefer code formatting when navigation does not help a +reader understand or choose the API. + +Use `@see` only for a verified related public API: a close alternative, +inverse, complement, level variant, or closely returned, consumed, or +configured type. Explain non-obvious relationships. Exclude implementation +dependencies, broad concepts, example-only helpers, private APIs, and merely +lexical matches. diff --git a/repos/effect/.agents/skills/jsdocs/examples.md b/repos/effect/.agents/skills/jsdocs/examples.md new file mode 100644 index 0000000000..2b78e6daf8 --- /dev/null +++ b/repos/effect/.agents/skills/jsdocs/examples.md @@ -0,0 +1,26 @@ +# Examples + +Examples are optional. Keep or add one only for behavior not evident from the +signature, meaningful composition, or useful inference or narrowing. Replace +or remove examples that are trivial, misleading, contrived, or +scaffolding-heavy. + +Use `**Example** (Unique use-case title)`, optional prose, and exactly one +non-empty `ts` fence. Titles must remain unique after trimming and lowercasing. + +Read `packages/tools/doctest/README.md` for runnable-fence and inline-assertion +syntax. Additionally: + +- Use public imports and arrange nontrivial examples as setup, operation, then + semantic observation. +- The transform does not run Effects or await promises automatically. Prefer + awaited `Effect.runPromise`; use `Effect.runSync` only when synchronous + execution is the documented contract. +- Keep type-level examples marked without tautological runtime assertions. +- Leave examples that register tests and intentionally non-executable examples + as plain `ts` fences. +- Use `Ref`, `Deferred`, or `Queue` rather than mutable probes for concurrency, + interruption, or races. + +If example research suggests an implementation or type bug, report it instead +of changing runtime code during a documentation-only pass. diff --git a/repos/effect/.agents/skills/migration-guidance/SKILL.md b/repos/effect/.agents/skills/migration-guidance/SKILL.md new file mode 100644 index 0000000000..752cba35e5 --- /dev/null +++ b/repos/effect/.agents/skills/migration-guidance/SKILL.md @@ -0,0 +1,21 @@ +--- +name: migration-guidance +description: Use when changing how a v3 public API maps to v4, editing migration annotations, or regenerating migration/v3-to-v4.md. +--- + +Update guidance when a v3 public module or API is renamed, moved, removed, +replaced, or gains a materially different v4 contract. A v4-only API without a +v3 counterpart does not automatically need an annotation. + +1. Account for every affected v3 symbol. +2. Read [annotations.md](annotations.md) before adding or changing annotation + YAML. Verify every suggested replacement against implementation and tests. +3. When checking or regenerating the reference, or when the API change exists in + a committed ref, read [generation.md](generation.md). Otherwise report that + generation is deferred. +4. When output was generated, inspect it for unrelated movement and stale refs. + +The task is complete when every affected v3 symbol is accounted for, annotation +replacements are verified, checks and generation succeed when the change is in +a committed ref, and every generated difference is explained. Report deferred +checks explicitly for uncommitted API changes. diff --git a/repos/effect/.agents/skills/migration-guidance/annotations.md b/repos/effect/.agents/skills/migration-guidance/annotations.md new file mode 100644 index 0000000000..ac5a1ca887 --- /dev/null +++ b/repos/effect/.agents/skills/migration-guidance/annotations.md @@ -0,0 +1,15 @@ +# Migration Annotations + +Add or update one YAML file per v3 module under `migration/annotations/`. Use +stable API IDs without trailing `#type` or `#value` facets: + +```yaml +effect/Effect#async: + replacement: Effect.callback + note: Use the callback constructor. + example: Effect.callback((resume) => resume(Effect.void)) +``` + +Every annotation requires `replacement` and `note`; `example` is optional. Use +`replacement: none` when there is no direct replacement and explain the +supported migration strategy instead of inventing an equivalent API. diff --git a/repos/effect/.agents/skills/migration-guidance/generation.md b/repos/effect/.agents/skills/migration-guidance/generation.md new file mode 100644 index 0000000000..2eeb6155c6 --- /dev/null +++ b/repos/effect/.agents/skills/migration-guidance/generation.md @@ -0,0 +1,14 @@ +# Generated Migration Reference + +`migration/v3-to-v4.md` is generated. Check annotations and regenerate using +explicit committed refs containing the change: + +```sh +pnpm api-diff --base-ref origin/v3 --head-ref HEAD --check +pnpm api-diff --base-ref origin/v3 --head-ref HEAD --write-doc migration/v3-to-v4.md +``` + +The API diff reads refs through detached worktrees. If the API change is +uncommitted, `HEAD` does not contain it. Update known annotation IDs, defer the +check and regeneration, and report the limitation. Do not create a temporary +commit solely to run the tool. diff --git a/repos/effect/.agents/skills/package-development/SKILL.md b/repos/effect/.agents/skills/package-development/SKILL.md new file mode 100644 index 0000000000..5b419895e5 --- /dev/null +++ b/repos/effect/.agents/skills/package-development/SKILL.md @@ -0,0 +1,35 @@ +--- +name: package-development +description: Package registration. Use when adding a workspace package, making a private package publishable, renaming a package, or moving its workspace path. +--- + +Treat package work as a registration change, not a directory copy. + +## Workflow + +1. Inspect the closest package in the same family. Classify publication status, + runtime, test environment, platform support, barrels, and dependency shape. + Justify each proposed file by precedent or an explicit repository need. + Continue when every proposed file is justified. +2. Create or move only required source, test, TypeScript, and manifest files. + When manifest dependencies change, read [dependencies.md](dependencies.md) + and classify every entry by role. + Continue when pnpm discovers the intended name and manifests reference no + absent file. +3. Read [registration.md](registration.md). Classify every registration surface + as applicable or not applicable and update every applicable surface. + Continue when every surface is classified and old references are explained. +4. For a published package, read [publishing.md](publishing.md) and verify its + development and packed surfaces. +5. Run codegen when the package owns generated barrels, then inspect generated + sections rather than editing them manually. + Continue when generated sections match source modules without manual edits. +6. Run focused package tests and scripts plus applicable root checks. Include + public API type tests when applicable. For published packages, build and dry + pack the package as described in [publishing.md](publishing.md). + Continue when focused and root checks pass and the packed surface is intended. +7. Apply root changeset, generated-file, documentation, and workflow routing. + +The task is complete when package discovery, registration, generated output, +validation, packed output, and changeset routing are all verified or reported +as not applicable. diff --git a/repos/effect/.agents/skills/package-development/dependencies.md b/repos/effect/.agents/skills/package-development/dependencies.md new file mode 100644 index 0000000000..45dd603f34 --- /dev/null +++ b/repos/effect/.agents/skills/package-development/dependencies.md @@ -0,0 +1,30 @@ +# Package Dependencies + +Inspect the closest package with the same publication and integration shape. +Classify every manifest entry by the role it serves: + +| Role | Use when | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `dependencies` | Published runtime code requires the package to receive its own installed copy. | +| `peerDependencies` | Consumers provide a compatible shared package or the public contract integrates with their copy. | +| `devDependencies` | Only repository build, test, type test, benchmark, or code generation needs the package. | +| `optionalDependencies` | A runtime feature handles absence and installation failure must not block the base package. | +| Optional peer via `peerDependenciesMeta` | A consumer-provided integration is genuinely optional. Keep its version in `peerDependencies`. | + +Use `workspace:^` for workspace packages unless neighboring packages establish a +different policy. When local build or tests need an installed peer, add the peer +to `devDependencies` at the repository's tested version while keeping the peer +range based on supported consumer versions. + +Derive external versions from current consumers with the same integration +shape. Add only direct requirements; do not copy a neighboring manifest entry +that the new package's runtime, declarations, build, or tests do not use. + +After editing manifests, run root `pnpm install`. Inspect warnings and the +package's `pnpm-lock.yaml` importer for intended specifiers and resolutions. If +a dependency has an install or native build, classify it under +`pnpm-workspace.yaml#allowBuilds` using current repository policy. + +This branch is complete when every entry has one justified role, workspace and +peer ranges follow repository policy, the lockfile importer agrees with the +manifest, and install/build policy is explicit. diff --git a/repos/effect/.agents/skills/package-development/publishing.md b/repos/effect/.agents/skills/package-development/publishing.md new file mode 100644 index 0000000000..0058c874fd --- /dev/null +++ b/repos/effect/.agents/skills/package-development/publishing.md @@ -0,0 +1,19 @@ +# Publishing A Package + +Compare the package manifest with a neighboring published package in the same +family. + +- Keep development `exports` and `publishConfig.exports` aligned by key. + Source targets become their intended built targets; blocked internal and + legacy paths remain blocked. +- Check current AI documentation copy tooling and the package `files` list for + every file required in the published payload. +- Ensure each public source entrypoint has exactly one published counterpart + and no internal entrypoint becomes exposed. + +Run the current root build so publication payload generation executes. Create a +tarball in a temporary destination without publishing, inspect its file list, +and inspect the packed `package.json` whenever manifest transformation matters. + +This branch is complete when package and root checks pass and the packed +surface contains exactly the intended files, exports, and metadata. diff --git a/repos/effect/.agents/skills/package-development/registration.md b/repos/effect/.agents/skills/package-development/registration.md new file mode 100644 index 0000000000..5fdf21a324 --- /dev/null +++ b/repos/effect/.agents/skills/package-development/registration.md @@ -0,0 +1,41 @@ +# Package Registration + +Use current configuration and the closest package in the same family as the +sources of truth. A package-local build does not prove repository registration. +Classify every row as applicable or not applicable. + +## Workspace And TypeScript + +| Surface | Check | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pnpm-workspace.yaml` | Its globs cover the package path and exact-name pnpm selection discovers it. | +| `pnpm-lock.yaml` | The current importer, name, and dependencies exist; renamed or moved importers are gone. | +| `tsconfig.packages.json` | Packages intended to participate in the root TypeScript project-reference graph are referenced. Follow neighboring package policy; buildable private tools are not automatically included. | +| `tsconfig.tests.json` | Broad test globs cover the path. Add source aliases only when tests require workspace source routing or would otherwise create a dependency cycle. Preserve intentional platform exclusions. | + +## Tests And Documentation + +| Surface | Check | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `vitest.config.ts` | A package with runtime tests has the intended project, environment, setup, and inclusion rules. | +| `tstyche.json` | Type-test discovery covers the package depth; current globs cover one- and two-level package layouts. | +| `vitest.docs.ts` | Doctest source discovery covers packages with runnable JSDoc examples; current globs cover one- and two-level package layouts. | +| `jsdocs.config.json` | Public source is included in JSDoc checks and exclusions by package family remain intentional. | +| `deno.json` | Deno checks cover the package family, or its exclusion is intentional. | + +## Release And Discovery + +| Surface | Check | +| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `.changeset/config.json` | Published fixed-group membership uses the current name; private tooling is not added solely because it builds. | +| `README.md` | Public catalog entries have the current name, path, description, and documentation link. | +| AI documentation copy tooling and package `files` | Published payloads include required generated documentation. Continue with [publishing.md](publishing.md) for the full packed-surface audit. | +| Snapshot publishing workflows | Snapshot publishing selects the package path. Existing globs may already cover it; apply root workflow requirements before editing. | +| Runtime-specific CI workflows | Runtime-specific packages participate in applicable runtime jobs; apply root workflow requirements before editing. | + +## Paths And Consumers + +Audit explicit workspace package names in root tooling manifests. Search scripts +for the package family, old name, and old path to find path-sensitive clean, +codemod, circularity, copy, and related behavior. Explain every remaining old +reference after a rename or move. diff --git a/repos/effect/.agents/skills/performance-analysis/SKILL.md b/repos/effect/.agents/skills/performance-analysis/SKILL.md new file mode 100644 index 0000000000..d21e7f4b3a --- /dev/null +++ b/repos/effect/.agents/skills/performance-analysis/SKILL.md @@ -0,0 +1,17 @@ +--- +name: performance-analysis +description: Performance analysis. Use for runtime throughput or latency benchmarks, TypeScript compiler cost, performance regressions, comparison harnesses, or thresholds. +--- + +Validate correctness with focused tests before measuring. Compare the same +focused operation and environment across revisions; a single unpaired run is +not evidence of improvement. + +Select every applicable branch: + +- **Runtime performance:** Read [runtime.md](runtime.md). +- **Type performance:** Read [types.md](types.md). + +The task is complete when every selected branch meets its measurement and +reporting criteria and applicable root checks pass or are reported as not +runnable. diff --git a/repos/effect/.agents/skills/performance-analysis/runtime.md b/repos/effect/.agents/skills/performance-analysis/runtime.md new file mode 100644 index 0000000000..501b5c900d --- /dev/null +++ b/repos/effect/.agents/skills/performance-analysis/runtime.md @@ -0,0 +1,19 @@ +# Runtime Performance + +For authoritative base/head comparisons or fixture work, read +`packages/effect/runtimeperf/README.md`. Use `pnpm runtimeperf-compare +` for local worktree comparisons and explicit `--base`/`--head` +only for committed refs. Report the resolved comparison head and measurement +configuration. + +For exploratory scripts under `benchmark/`, match nearby Tinybench scripts, +keep setup outside the measured callback unless setup is the operation, warm +reusable state, close resources, and print the table. Label results exploratory +unless the task defines a repeatable comparison protocol. + +Focused runtimeperf work is complete when fixture validation and the selected +run succeed. Comparison work additionally requires every configured round to +succeed, the paired statistical classification to support the conclusion, and +resolved refs and settings to be reported. Harness changes require its focused +checks. Tinybench work is complete when resources close, the table is reported, +and claims remain exploratory unless a repeatable protocol was run. diff --git a/repos/effect/.agents/skills/performance-analysis/types.md b/repos/effect/.agents/skills/performance-analysis/types.md new file mode 100644 index 0000000000..2028c2d6b5 --- /dev/null +++ b/repos/effect/.agents/skills/performance-analysis/types.md @@ -0,0 +1,17 @@ +# Type Performance + +Read `packages/effect/typeperf/README.md` before adding fixtures, changing the +harness, updating thresholds, or running cross-ref comparisons. Isolate one +realistic public type path and compare the same fixture and compiler environment +across revisions. Derive supported suites and comparison commands from the +current harness documentation and CLI. + +Before changing a threshold, record its previous value and measured result, +explain the expected delta, update it, and run a clean focused verification. +Keep ordinary threshold fixtures separate from cross-ref fixtures. + +This branch is complete when the focused measurement is repeatable, threshold +verification is clean when applicable, and reported comparisons include +resolved refs, compiler version, and an explanation of the observed delta. +Fixture or harness changes also require the focused validation and formatting +checks documented in the README. diff --git a/repos/effect/.agents/skills/scratchpad/SKILL.md b/repos/effect/.agents/skills/scratchpad/SKILL.md deleted file mode 100644 index b42bdc88fa..0000000000 --- a/repos/effect/.agents/skills/scratchpad/SKILL.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: scratchpad -description: Extract the JSDoc example nearest the active source selection or cursor into ./scratchpad as a TypeScript file. Use when the user asks to dump, copy, open, or try a source example in scratchpad. ---- - -Use this skill to create a scratchpad TypeScript file from the JSDoc `**Example**` -nearest the user's active source cursor or selection. - -## Workflow - -1. Determine the source path and line: - - Use the IDE active file and selection/cursor line when present. - - Use an explicit file and line when the user provides them. - - If no line or selection is available, ask for it. -2. Run: - - ```sh - node .agents/skills/scratchpad/scripts/extract-example.mjs - ``` - -3. If the script exits with code 2 because there is no obvious runner, ask the - user whether to preserve the example exactly, name an Effect value to run, or - cancel. - - To preserve exactly, rerun with `--mode preserve`. - - To run a specific Effect value, rerun with `--runner `. -4. Report the created path as a clickable file link. This is the deterministic - way to open it in the code pane. -5. Do not run the scratchpad file unless the user explicitly asks. - -## Behavior - -- The script chooses the example whose `**Example**` section contains the line; - otherwise it chooses the first following example; otherwise the nearest - previous example. -- Filenames are derived from the source file and example title, for example - `scratchpad/Schedule-retrying-and-repeating-effects.ts`. -- Existing files are not overwritten. The script appends a numeric suffix. -- In auto mode, if a top-level `program` binding exists, the script appends: - - ```ts - Effect.runPromise(program).then(console.log, console.error) - ``` - -- If the example already contains an Effect runner, the script preserves it. diff --git a/repos/effect/.agents/skills/scratchpad/agents/openai.yaml b/repos/effect/.agents/skills/scratchpad/agents/openai.yaml deleted file mode 100644 index f83a2a4b08..0000000000 --- a/repos/effect/.agents/skills/scratchpad/agents/openai.yaml +++ /dev/null @@ -1,7 +0,0 @@ -interface: - display_name: "Scratchpad" - short_description: "Extract examples into scratchpad" - default_prompt: "Use $scratchpad to extract the active JSDoc example into scratchpad." - -policy: - allow_implicit_invocation: true diff --git a/repos/effect/.agents/skills/scratchpad/scripts/extract-example.mjs b/repos/effect/.agents/skills/scratchpad/scripts/extract-example.mjs deleted file mode 100644 index e668122922..0000000000 --- a/repos/effect/.agents/skills/scratchpad/scripts/extract-example.mjs +++ /dev/null @@ -1,260 +0,0 @@ -#!/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/.agents/skills/test-development/SKILL.md b/repos/effect/.agents/skills/test-development/SKILL.md new file mode 100644 index 0000000000..3c6082e7ca --- /dev/null +++ b/repos/effect/.agents/skills/test-development/SKILL.md @@ -0,0 +1,13 @@ +--- +name: test-development +description: Test development. Use when adding or changing runtime Vitest coverage or Tstyche contracts for behavior, inference, assignability, or displayed public types. +--- + +Select every applicable branch before editing: + +- **Runtime behavior:** Read [runtime.md](runtime.md). +- **Type contracts:** Read [types.md](types.md). + +Some changes may require both branches. The task is complete when every +changed runtime behavior and type contract has focused coverage, targeted tests +pass, and applicable root checks pass or are reported as not runnable. diff --git a/repos/effect/.agents/skills/test-development/displayed-types.md b/repos/effect/.agents/skills/test-development/displayed-types.md new file mode 100644 index 0000000000..6edcc977a0 --- /dev/null +++ b/repos/effect/.agents/skills/test-development/displayed-types.md @@ -0,0 +1,20 @@ +# Displayed Types + +Deliberately produce an assignment error and match a distinctive substring with +Tstyche's checked `@ts-expect-error` message: + +```ts +it("simplifies the displayed type", () => { + const value = null as unknown as PublicType + + // @ts-expect-error Type '{ readonly value: string; }' + const displayed: never = value + + void displayed +}) +``` + +Keep the expected substring as small as possible while distinguishing the +public type from the leaked implementation type. Before accepting the test, +temporarily restore the broken type and confirm the diagnostic-message match +fails. diff --git a/repos/effect/.agents/skills/test-development/runtime.md b/repos/effect/.agents/skills/test-development/runtime.md new file mode 100644 index 0000000000..864755bef9 --- /dev/null +++ b/repos/effect/.agents/skills/test-development/runtime.md @@ -0,0 +1,13 @@ +# Runtime Tests + +- Use `it.effect` for Effect-returning tests and regular `it` for pure + synchronous tests. +- `it.effect` and `it.live` provide and close a `Scope`; return scoped effects + directly instead of wrapping the body in `Effect.scoped`. +- Use `assert` from `@effect/vitest`, not Vitest's `expect`. +- Use `TestClock` for time-dependent behavior. +- Keep `Effect.runSync` out of unit tests; runnable documentation follows the + root JSDoc validation rules. + +Inspect nearby tests for imports and structure. This branch is complete when +every changed behavior has focused coverage and its targeted test passes. diff --git a/repos/effect/.agents/skills/test-development/types.md b/repos/effect/.agents/skills/test-development/types.md new file mode 100644 index 0000000000..3bc747789e --- /dev/null +++ b/repos/effect/.agents/skills/test-development/types.md @@ -0,0 +1,14 @@ +# Type Tests + +Inspect nearby `.tst.ts` files and use their imports and assertion style. Use +ordinary Tstyche assertions such as `toBe` for structural equality and choose a +specific assertion for the inference or assignability contract under test. + +Structural equality does not verify editor quick-info rendering. For internal +aliases, unsimplified intersections, or other displayed-type regressions, read +[displayed-types.md](displayed-types.md). + +Run targeted `pnpm test-types `; the root command covers every +configured TypeScript version. For a regression fix, confirm the assertion +fails against the pre-fix type. This branch is complete when the assertion +proves the intended contract and the target passes. diff --git a/repos/effect/.agents/skills/vendored-assets/SKILL.md b/repos/effect/.agents/skills/vendored-assets/SKILL.md new file mode 100644 index 0000000000..6c84d0a826 --- /dev/null +++ b/repos/effect/.agents/skills/vendored-assets/SKILL.md @@ -0,0 +1,47 @@ +--- +name: vendored-assets +description: Vendored assets. Use when importing or updating checked-in third-party or externally generated JavaScript, CSS, registries, schemas, snapshots, or Scalar, Swagger, and MIME artifacts. +--- + +Treat upstream artifacts as supply-chain inputs. Change their generator or +documented import source, then regenerate; generated output is review evidence, +not an editing surface. + +## Workflow + +1. **Trace ownership.** Identify the artifact, generator or exact import + procedure, upstream source/version, license, consumers, tests, and shipped + package or bundle. Search by asset and upstream project name to find current + packaging scripts, generated artifacts, consumers, and history. + Continue when every input and consumer is accounted for. +2. **Pin provenance.** Resolve moving URLs, tags, branches, and omitted versions + to an immutable release or artifact. Record project, version, path or URL, + and digest in the generator or maintenance header. Omit a digest only when + another enforced immutable source is recorded. + Continue when a future run cannot silently select different bytes. +3. **Clear the license gate.** Verify release metadata, license files, bundled + notices, and the license trail retained in the distributed form. + Continue when every distributed artifact has a verified retained license trail. +4. **Regenerate.** Improve the generator or import reference first. Recover an + exact procedure for generatorless assets; add a deterministic script only + for recurring updates. Record a one-off procedure beside the artifact. A + clean rerun must produce no diff. +5. **Audit the complete diff.** Account for behavior, URLs and runtime fetches, + source maps, notices, encoding, format changes, and additions or removals. + For browser assets, also read [browser-assets.md](browser-assets.md). + Continue when every change is attributable and suspicious content is resolved + or reported. +6. **Clear test and size gates.** Record byte sizes before and after, use + focused consumer tests, and compare bundle size or composition when shipped + JavaScript, CSS, or registry output changes materially. Compare that output + against the pre-update revision rather than measuring only current size. + Continue when focused checks pass and every non-trivial size delta is explained. +7. **Close release impact.** Apply root changeset routing for consumer-visible + behavior, browser support, wire data, or meaningful shipped-size changes. + Generated sections owned by barrels, AI docs, or migration tooling remain + with the owners named in root instructions. + +The task is complete when every input and consumer is accounted for, provenance +and licenses are immutable and retained, regeneration is deterministic, every +output change and non-trivial size delta is explained, focused checks pass or +are reported as not runnable, and release impact is recorded. diff --git a/repos/effect/.agents/skills/vendored-assets/browser-assets.md b/repos/effect/.agents/skills/vendored-assets/browser-assets.md new file mode 100644 index 0000000000..a95b21fe72 --- /dev/null +++ b/repos/effect/.agents/skills/vendored-assets/browser-assets.md @@ -0,0 +1,13 @@ +# Browser Asset Audit + +For browser-delivered JavaScript or CSS, inspect the complete generated output +for: + +- CSP requirements and dynamic code execution; +- injected markup, scripts, or styles; +- network destinations and runtime loading; +- external URLs, source maps, and runtime fetches; +- bundled code or notices absent from upstream top-level metadata. + +Every destination and executable behavior must be attributable to the selected +upstream release. Resolve or report suspicious content before completion. diff --git a/repos/effect/.changeset/ai-approved-tool-results.md b/repos/effect/.changeset/ai-approved-tool-results.md new file mode 100644 index 0000000000..203cf0284f --- /dev/null +++ b/repos/effect/.changeset/ai-approved-tool-results.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Retain completed tool approval results in non-streaming responses so Chat records them and does not replay approved tools on later turns. diff --git a/repos/effect/.changeset/align-type-id-paths.md b/repos/effect/.changeset/align-type-id-paths.md new file mode 100644 index 0000000000..932d2d2c02 --- /dev/null +++ b/repos/effect/.changeset/align-type-id-paths.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/opentelemetry": patch +--- + +Align runtime type IDs with their module paths. Effect markers now omit legacy grouping prefixes and the `unstable` path segment, while OpenTelemetry spans use the `OtelTracer` module path. Custom implementations that copy these marker strings must adopt the corrected IDs. diff --git a/repos/effect/.changeset/all-union-record-channels.md b/repos/effect/.changeset/all-union-record-channels.md new file mode 100644 index 0000000000..ddafd3ab60 --- /dev/null +++ b/repos/effect/.changeset/all-union-record-channels.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.all` to retain errors and required services from every branch of a union of record inputs. diff --git a/repos/effect/.changeset/anthropic-image-strings.md b/repos/effect/.changeset/anthropic-image-strings.md new file mode 100644 index 0000000000..7cb1c1467c --- /dev/null +++ b/repos/effect/.changeset/anthropic-image-strings.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Preserve base64 image strings in Anthropic requests. diff --git a/repos/effect/.changeset/anthropic-strict-json-schema.md b/repos/effect/.changeset/anthropic-strict-json-schema.md new file mode 100644 index 0000000000..9a1caae535 --- /dev/null +++ b/repos/effect/.changeset/anthropic-strict-json-schema.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Exclude the provider-only `strictJsonSchema` option from Anthropic Messages request bodies while preserving its control over tool strictness. diff --git a/repos/effect/.changeset/anthropic-structured-output-fallback.md b/repos/effect/.changeset/anthropic-structured-output-fallback.md new file mode 100644 index 0000000000..3ed06de122 --- /dev/null +++ b/repos/effect/.changeset/anthropic-structured-output-fallback.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix non-native structured output generation by always requesting the response tool and excluding accompanying prose when decoding its JSON payload. diff --git a/repos/effect/.changeset/arbitrary-index-constraints.md b/repos/effect/.changeset/arbitrary-index-constraints.md new file mode 100644 index 0000000000..9cff5c7daf --- /dev/null +++ b/repos/effect/.changeset/arbitrary-index-constraints.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `Arbitrary.schema` to respect applicable index signatures when generating and shrinking object properties, including fixed fields in `Schema.StructWithRest` and overlapping records. + +Combine compatible string, number, and bigint constraints during generation so cases such as a `String` field constrained by a `NonEmptyString` record remain productive at size zero. Other intersections are validated and may exhaust the discard budget. diff --git a/repos/effect/.changeset/array-ensure-array-elements.md b/repos/effect/.changeset/array-ensure-array-elements.md new file mode 100644 index 0000000000..762cc5afa9 --- /dev/null +++ b/repos/effect/.changeset/array-ensure-array-elements.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schema.ArrayEnsure` to preserve array-valued element branches and outer-array encoding cardinality. diff --git a/repos/effect/.changeset/atom-rpc-query-requires.md b/repos/effect/.changeset/atom-rpc-query-requires.md new file mode 100644 index 0000000000..7169977a49 --- /dev/null +++ b/repos/effect/.changeset/atom-rpc-query-requires.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `AtomRpc.query` returning `never` for RPCs whose middleware declares service `requires`. The return-type conditional now infers all six `Rpc` type parameters, matching `mutation` and every utility in `Rpc`. diff --git a/repos/effect/.changeset/atom-solid-idle-ttl.md b/repos/effect/.changeset/atom-solid-idle-ttl.md new file mode 100644 index 0000000000..e7a9989e5e --- /dev/null +++ b/repos/effect/.changeset/atom-solid-idle-ttl.md @@ -0,0 +1,5 @@ +--- +"@effect/atom-solid": patch +--- + +Allow `RegistryProvider` to leave `defaultIdleTTL` undefined, matching the React binding and enabling immediate cleanup of unused atoms unless a TTL is explicitly configured. diff --git a/repos/effect/.changeset/bright-mimes-leave.md b/repos/effect/.changeset/bright-mimes-leave.md new file mode 100644 index 0000000000..09ca9859ab --- /dev/null +++ b/repos/effect/.changeset/bright-mimes-leave.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-node": patch +"effect": patch +--- + +Remove the `mime` runtime dependency. The new `effect/unstable/http/Mime` module provides top-level lookup functions +backed by a vendored standard MIME registry. diff --git a/repos/effect/.changeset/bright-sockets-connect.md b/repos/effect/.changeset/bright-sockets-connect.md new file mode 100644 index 0000000000..ca176f3810 --- /dev/null +++ b/repos/effect/.changeset/bright-sockets-connect.md @@ -0,0 +1,9 @@ +--- +"@effect/ai-openai": patch +"@effect/platform-bun": patch +"@effect/platform-node": patch +"effect": patch +--- + +Allow sockets to use browser, Bun, and Node WebSocket implementations without consumer casts. Platform constructors +now support typed opening-handshake headers where available. diff --git a/repos/effect/.changeset/browser-crypto-missing-subtle.md b/repos/effect/.changeset/browser-crypto-missing-subtle.md new file mode 100644 index 0000000000..0fedd8a2b0 --- /dev/null +++ b/repos/effect/.changeset/browser-crypto-missing-subtle.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Return a typed `PlatformError` from `BrowserCrypto` digest operations when `crypto.subtle` is unavailable, instead of failing with a defect. diff --git a/repos/effect/.changeset/browser-indexeddb-binary-key-existence.md b/repos/effect/.changeset/browser-indexeddb-binary-key-existence.md new file mode 100644 index 0000000000..c5462e706a --- /dev/null +++ b/repos/effect/.changeset/browser-indexeddb-binary-key-existence.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Fix `BrowserKeyValueStore.layerIndexedDb` to report keys containing `Uint8Array` values from `has`. diff --git a/repos/effect/.changeset/browser-runtime-custom-teardown.md b/repos/effect/.changeset/browser-runtime-custom-teardown.md new file mode 100644 index 0000000000..443d692f7d --- /dev/null +++ b/repos/effect/.changeset/browser-runtime-custom-teardown.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Invoke custom `BrowserRuntime.runMain` teardown callbacks when the main effect completes, and remove the `pagehide` listener once the main fiber finishes. diff --git a/repos/effect/.changeset/bun-http-server-unix-address.md b/repos/effect/.changeset/bun-http-server-unix-address.md new file mode 100644 index 0000000000..b5abe92de5 --- /dev/null +++ b/repos/effect/.changeset/bun-http-server-unix-address.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Report Unix socket addresses from Bun HTTP servers. diff --git a/repos/effect/.changeset/bun-stream-preserve-failure.md b/repos/effect/.changeset/bun-stream-preserve-failure.md new file mode 100644 index 0000000000..c5756f3ee9 --- /dev/null +++ b/repos/effect/.changeset/bun-stream-preserve-failure.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Preserve mapped errors from Bun readable streams. diff --git a/repos/effect/.changeset/cache-invalidate-when-replacement.md b/repos/effect/.changeset/cache-invalidate-when-replacement.md new file mode 100644 index 0000000000..b8b861c282 --- /dev/null +++ b/repos/effect/.changeset/cache-invalidate-when-replacement.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cache.invalidateWhen` and `ScopedCache.invalidateWhen` deleting a replacement entry while waiting for an earlier lookup. diff --git a/repos/effect/.changeset/cache-refresh-cancellation-ownership.md b/repos/effect/.changeset/cache-refresh-cancellation-ownership.md new file mode 100644 index 0000000000..7ad98b91c6 --- /dev/null +++ b/repos/effect/.changeset/cache-refresh-cancellation-ownership.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix interruption of `Cache.refresh` for a missing key removing a newer value written by `Cache.set`. diff --git a/repos/effect/.changeset/cache-refresh-capacity.md b/repos/effect/.changeset/cache-refresh-capacity.md new file mode 100644 index 0000000000..d03ac1d50d --- /dev/null +++ b/repos/effect/.changeset/cache-refresh-capacity.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cache.refresh` and `ScopedCache.refresh` exceeding capacity when an existing key is evicted while its refresh is in progress. Publishing the refreshed entry now evicts older entries as needed, releasing their resources in `ScopedCache`. diff --git a/repos/effect/.changeset/cache-refresh-zero-ttl-ownership.md b/repos/effect/.changeset/cache-refresh-zero-ttl-ownership.md new file mode 100644 index 0000000000..06f9ee7f25 --- /dev/null +++ b/repos/effect/.changeset/cache-refresh-zero-ttl-ownership.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cache.refresh` for an initially missing key deleting a newer cached value when the refresh completes with zero time to live. diff --git a/repos/effect/.changeset/cache-synchronous-interruption.md b/repos/effect/.changeset/cache-synchronous-interruption.md new file mode 100644 index 0000000000..23fde1c27d --- /dev/null +++ b/repos/effect/.changeset/cache-synchronous-interruption.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent `Cache` from retaining synchronously interrupted lookups. diff --git a/repos/effect/.changeset/calm-ducks-fail.md b/repos/effect/.changeset/calm-ducks-fail.md new file mode 100644 index 0000000000..7c801d3218 --- /dev/null +++ b/repos/effect/.changeset/calm-ducks-fail.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Wake `NodeStream.pipeThroughDuplex` readers when the upstream fails so the original error is propagated instead of hanging. diff --git a/repos/effect/.changeset/calm-headers-hide.md b/repos/effect/.changeset/calm-headers-hide.md new file mode 100644 index 0000000000..0e3cff5d52 --- /dev/null +++ b/repos/effect/.changeset/calm-headers-hide.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Respect custom HTTP header redaction when recording server span attributes. diff --git a/repos/effect/.changeset/calm-readers-transact.md b/repos/effect/.changeset/calm-readers-transact.md new file mode 100644 index 0000000000..011717a3c4 --- /dev/null +++ b/repos/effect/.changeset/calm-readers-transact.md @@ -0,0 +1,7 @@ +--- +"@effect/sql-sqlite-bun": patch +"@effect/sql-sqlite-node": patch +--- + +Allow read-only SQLite clients to use `withTransaction` when `PRAGMA query_only` is enabled. Writable clients continue +to reserve the writer lock when a transaction starts. diff --git a/repos/effect/.changeset/calm-trees-generate.md b/repos/effect/.changeset/calm-trees-generate.md new file mode 100644 index 0000000000..8179f0ba79 --- /dev/null +++ b/repos/effect/.changeset/calm-trees-generate.md @@ -0,0 +1,120 @@ +--- +"@effect/vitest": patch +"effect": patch +--- + +Add the experimental Schema-first `effect/unstable/arbitrary/Arbitrary` module for native generation without +fast-check. `Arbitrary.schema` derives an opaque arbitrary from the decoded Schema `Type`, `Arbitrary.sampleEffect` +provides interruptible sampling with typed exhaustion, and `Arbitrary.checkEffect` returns structured property results. +The initial implementation supports bounded discards, shrinking, replay, and recursive and mutually recursive Schemas. +`SampleError` and `Exhausted` include the effective seed so discarded runs remain reproducible even when the caller did +not provide one. `Arbitrary.isArbitrary` identifies values through the module's nominal protocol. Numeric constraints +retain `NaN` when it is accepted by their supported `Order.Number` bounds. Union derivation validates `oneOf` +exclusivity and isolates lazy cross-member shrinking from unrelated random generation. Object derivation keeps +optional-property selection constructive when candidate fields have different recursive costs. +Struct, Record, JSON-object, and record-shaped `Arbitrary.all` outputs periodically use a null prototype as an edge +case, preserving that prototype throughout shrinking and replay without perturbing structural PRNG choices. The change +adds 0.01–0.03 KB gzip to representative Arbitrary fixtures and leaves production-only bundle sentinels unchanged. + +Add `Arbitrary.map`, `Arbitrary.flatMap`, `Arbitrary.filter`, `Arbitrary.filterMap`, and `Arbitrary.all` for composing +derived Arbitraries without exposing a second catalog of primitive constructors. Filtering remains bounded and +promotes valid shrink descendants through rejected nodes. `maxShrinks` bounds every inspected shrink candidate, +including candidates rejected before property evaluation, while retaining the best shrunk input found when the +budget is exhausted. `flatMap` provides deterministic dependent generation, source-first shrinking, post-source PRNG +checkpoints, and one shared residual recursion budget. `all` combines tuples, iterables, and records with a shared +budget, randomized internal generation order, stable output shape, and independent member shrinking. Arbitrary values +implement `Pipeable` for composition with data-last combinators. + +Add the experimental Schema `arbitraryConstraint` and `toCodecArbitrary` annotations and their +`Schema.Annotations.ToArbitrary` types. Declarations can provide a Schema Link optimized for generation, while filters +can contribute native semantic constraints. The callback receives decoded type parameters and normalized constraints. +The compiler owns efficient representations for common built-ins, including JSON, RegExp, URL, Date, byte arrays, +ReadonlyMap, and ReadonlySet. Effect-specific HashMap, HashSet, Chunk, Graph, BigDecimal, and date-time declarations keep +local generation Links, while declarations with productive canonical codecs require no arbitrary-specific annotation. +`Schema.isUniqueKey` provides key-based Map uniqueness for explicit array representations. + +The same ownership policy applies to formatter and equivalence derivation: implementations for common declarations +live in their compiler, while domain-specific and dynamically constructed declarations retain local annotations. +Declarations whose intrinsic `Equal` implementation already matches their Schema equivalence need no annotation or +compiler special case. This keeps unused common callbacks out of production Schema bundles. + +Against the previous layout, `schema-toArbitrary` decreases from 36.68 KB to 33.24 KB gzip and +`arbitrary-combinators` decreases from 37.16 KB to 33.70 KB. `schema-toFormatter` increases from 18.92 KB to 19.49 KB +and `schema-toEquivalence` increases from 19.05 KB to 19.39 KB because callers that explicitly derive these capabilities +now retain the common declaration handlers. Generic production fixtures remain unchanged; an equivalence-specific +production fixture using common declarations decreases from 20.75 KB to 20.48 KB, while declarations whose intrinsic +equality is sufficient decrease from 23.42 KB to 23.34 KB. An Arbitrary-specific production fixture using common +declarations decreases from 20.35 KB to 19.61 KB, while one using the locally annotated BigDecimal and date-time +declarations increases from 18.34 KB to 23.01 KB. +The complete 31-scenario native Arbitrary comparison reports no statistically classified runtime regression; the five +moved BigDecimal and date-time scenarios remain within measurement noise. + +Add `SchemaGetter.forbiddenEncoding`, a reusable getter for the encode side of decode-only Schema transformations. + +Remove the fast-check bridge from the `effect` package, including `Schema.toArbitrary` and +`effect/testing/FastCheck`. Replace the legacy `Schema.Annotations.ToArbitrary` callback contract with the native +Schema-first types. The `effect` package no longer depends on fast-check. + +Migrate `TestSchema.Asserts.verifyLosslessTransformation` and `TestSchema.Asserts.arbitrary().verifyGeneration` to the +native runner. Both methods now accept native check options directly, bound unsuccessful generation, and include the +shrunk input and replay token in property failures. + +Use the Arbitrary runner for all `@effect/vitest` property tests. Property inputs may combine Schemas and Arbitraries, +and are composed directly with `Arbitrary.all`; check options are available through `arbitrary`. Raw fast-check +arbitraries and the `fastCheck` options object are no longer supported. As with the previous fast-check adapter, thrown +exceptions, defects, and typed failures from a property are shrinkable falsifications; Effect interruption remains an +interruption. + +Optimize constructive regular-expression generation by caching feasible lengths on the compiled pattern, computing +sequence-suffix feasibility once, and precomputing character-class metadata. Seeded generation, shrinking, and replay +remain unchanged. + +Optimize `BigDecimal.Order` and `BigDecimal.Equivalence` with a shared hybrid comparator. Ordinary scale differences +use cached, bounded coefficient alignment, while large differences are compared without materializing their decimal +zeroes. `BigDecimal.make` now rejects scales that are not safe integers. + +Before its removal, the materialized fast-check bridge fixture +`schema-toArbitrary-materialized-fast-check.ts` measured 79.00 KB minified and gzipped. + +Representative runtime measurements against corresponding hand-written fast-check 4.9.0 arbitraries are shown below. +Values are median latency on Node 24.12.0 and Apple M3; lower is better. Both implementations validate the +same output domains, although their generation distributions are not identical. Native speedup is fast-check latency +divided by Native latency, so higher is better. + +| Scenario | fast-check | Native | Native speedup | +| ----------------------------------- | ---------: | ------: | -------------: | +| 32 recursive samples | 150 µs | 103 µs | 1.45x | +| 128 optional Struct samples | 244 µs | 86.0 µs | 2.84x | +| 128 constrained strings | 742 µs | 49.7 µs | 14.86x | +| RegExp derivation and first sample | 13.4 ms | 30.8 µs | 429.02x | +| 64 RegExp strings | 595 µs | 919 µs | 0.64x | +| RegExp failure and shrinking | 168 µs | 88.2 µs | 1.91x | +| 128 bounded numbers | 68.9 µs | 21.8 µs | 3.18x | +| 128 `Uint8Array` samples | 98.3 µs | 74.4 µs | 1.32x | +| 128 `BigDecimal` samples | 66.6 µs | 56.3 µs | 1.18x | +| 128 `DateTime.Utc` samples | 71.2 µs | 50.5 µs | 1.42x | +| 128 named time zones | 52.2 µs | 27.9 µs | 1.85x | +| 128 time zones | 63.7 µs | 33.8 µs | 1.89x | +| 128 zoned date-times | 130 µs | 112 µs | 1.16x | +| 32 samples through Schema filter | 65.9 µs | 49.4 µs | 1.33x | +| 32 unique arrays | 156 µs | 132 µs | 1.18x | +| 128 literal samples | 40.0 µs | 3.70 µs | 10.78x | +| 128 mapped samples | 59.0 µs | 14.1 µs | 4.21x | +| 128 samples through passing filter | 58.9 µs | 13.9 µs | 4.23x | +| 32 samples through selective filter | 66.1 µs | 42.9 µs | 1.54x | +| 128 `filterMap` samples | 75.7 µs | 31.5 µs | 2.40x | +| Filtered failure and shrinking | 12.7 µs | 7.71 µs | 1.66x | +| 128 `all` tuple samples | 43.5 µs | 18.5 µs | 2.35x | +| 128 `all` record samples | 81.0 µs | 30.4 µs | 2.66x | +| 128 dependent `flatMap` samples | 125 µs | 67.2 µs | 1.86x | +| `flatMap` failure and shrinking | 20.1 µs | 6.71 µs | 2.99x | +| Replay `flatMap` shrink path | 14.3 µs | 6.57 µs | 2.17x | +| Passing property, 100 runs | 42.3 µs | 27.1 µs | 1.56x | +| `TestSchema`, 100 generations | 44.5 µs | 35.9 µs | 1.24x | +| First failure plus one shrink | 8.77 µs | 1.30 µs | 6.75x | +| Replay recorded failure | 6.35 µs | 1.19 µs | 5.36x | + +Cold recursive derivation is not included because the native fixture constructs and compiles a Schema, while the +fast-check fixture constructs a hand-written arbitrary; it is not a like-for-like warm-generator comparison. + +Add a guide for the native module and a migration guide from the fast-check bridge published in `effect@4.0.0-rc.109`. diff --git a/repos/effect/.changeset/catch-stream-channel-defects.md b/repos/effect/.changeset/catch-stream-channel-defects.md new file mode 100644 index 0000000000..6757799f40 --- /dev/null +++ b/repos/effect/.changeset/catch-stream-channel-defects.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Stream.catchDefect` and `Channel.catchDefect` for recovering from defects without catching typed failures or interruptions. diff --git a/repos/effect/.changeset/channel-rundone-completion.md b/repos/effect/.changeset/channel-rundone-completion.md new file mode 100644 index 0000000000..c0a74a290a --- /dev/null +++ b/repos/effect/.changeset/channel-rundone-completion.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove `Channel.runDone`; use `Channel.runDrain` to consume all output and return the completion value. diff --git a/repos/effect/.changeset/child-process-astral-escape-arguments.md b/repos/effect/.changeset/child-process-astral-escape-arguments.md new file mode 100644 index 0000000000..f68fd00493 --- /dev/null +++ b/repos/effect/.changeset/child-process-astral-escape-arguments.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve astral Unicode escapes and following arguments in `ChildProcess.make` and `ChildProcess.prefix` template literals. diff --git a/repos/effect/.changeset/childprocess-process-group-wait.md b/repos/effect/.changeset/childprocess-process-group-wait.md new file mode 100644 index 0000000000..0a6f55a4e0 --- /dev/null +++ b/repos/effect/.changeset/childprocess-process-group-wait.md @@ -0,0 +1,10 @@ +--- +"@effect/platform-node-shared": patch +"effect": patch +--- + +Wait for Node child process groups to exit during scoped release and `kill`. + +After signalling a process group, both operations now wait for its leader and descendants. Without `forceKillAfter`, the wait is limited to one second and never escalates. With `forceKillAfter`, the group receives `SIGKILL` at the deadline, followed by a final wait of up to one second. Native timers keep escalation working under a `TestClock`, and cleanup no longer depends on stdio closing. + +`exitCode` and `isRunning` remain tied to the leader's exit, and a leader that already exited successfully still leaves its group untouched. Process group checks count zombies, so cleanup may wait for the full bound under a non-reaping PID 1. diff --git a/repos/effect/.changeset/chunk-slice-concatenation.md b/repos/effect/.changeset/chunk-slice-concatenation.md new file mode 100644 index 0000000000..593f5c2a66 --- /dev/null +++ b/repos/effect/.changeset/chunk-slice-concatenation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Chunk` concatenation to preserve sliced elements. diff --git a/repos/effect/.changeset/clean-config-names.md b/repos/effect/.changeset/clean-config-names.md new file mode 100644 index 0000000000..0128f06601 --- /dev/null +++ b/repos/effect/.changeset/clean-config-names.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Rename the built-in `Config` constructors to PascalCase and rename `Config.mapOrFail` to `Config.mapEffect`. `Config.Array` and `Config.Record` now construct configs directly, with overloads for pathless options or a path followed by options, while their specialized schemas and the other built-in schemas are kept internal. + +This is a breaking naming cleanup for the Effect 4 release candidate. It makes casing consistently identify typed config constructors, aligns effectful mapping with the rest of the library, and prevents implementation schemas from expanding the public `Config` interface. diff --git a/repos/effect/.changeset/clean-yaks-parse.md b/repos/effect/.changeset/clean-yaks-parse.md new file mode 100644 index 0000000000..baf2d838cb --- /dev/null +++ b/repos/effect/.changeset/clean-yaks-parse.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Use Effect's YAML parser for OpenAPI patch files and remove the direct `yaml` dependency. diff --git a/repos/effect/.changeset/cleanup-before-use-callbacks.md b/repos/effect/.changeset/cleanup-before-use-callbacks.md new file mode 100644 index 0000000000..414cb54e64 --- /dev/null +++ b/repos/effect/.changeset/cleanup-before-use-callbacks.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `Effect.acquireUseRelease` releases an acquired resource and `Effect.useSpan` ends its span when the use callback throws before returning an effect. The thrown exception remains a defect, but no longer skips cleanup. diff --git a/repos/effect/.changeset/clear-wings-relax.md b/repos/effect/.changeset/clear-wings-relax.md new file mode 100644 index 0000000000..526b3bc308 --- /dev/null +++ b/repos/effect/.changeset/clear-wings-relax.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +use `new` instantiation for streams diff --git a/repos/effect/.changeset/cli-completion-command-aliases.md b/repos/effect/.changeset/cli-completion-command-aliases.md new file mode 100644 index 0000000000..00d824e98e --- /dev/null +++ b/repos/effect/.changeset/cli-completion-command-aliases.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve flags and nested commands when completing a CLI subcommand through its alias. diff --git a/repos/effect/.changeset/cli-completion-shared-flags.md b/repos/effect/.changeset/cli-completion-shared-flags.md new file mode 100644 index 0000000000..9e27bd98dd --- /dev/null +++ b/repos/effect/.changeset/cli-completion-shared-flags.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Include inherited shared flags in descendant CLI completions. diff --git a/repos/effect/.changeset/cli-key-value-pair-first-separator.md b/repos/effect/.changeset/cli-key-value-pair-first-separator.md new file mode 100644 index 0000000000..d8241fd5a5 --- /dev/null +++ b/repos/effect/.changeset/cli-key-value-pair-first-separator.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow `=` in values parsed by `Primitive.keyValuePair`, `Flag.keyValuePair`, and `Param.keyValuePair` in `effect/unstable/cli`. diff --git a/repos/effect/.changeset/cli-optional-alternative-flags.md b/repos/effect/.changeset/cli-optional-alternative-flags.md new file mode 100644 index 0000000000..743c995ff1 --- /dev/null +++ b/repos/effect/.changeset/cli-optional-alternative-flags.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow optional alternative CLI flags. diff --git a/repos/effect/.changeset/cli-scalar-constructor-names.md b/repos/effect/.changeset/cli-scalar-constructor-names.md new file mode 100644 index 0000000000..e5476bb8a2 --- /dev/null +++ b/repos/effect/.changeset/cli-scalar-constructor-names.md @@ -0,0 +1,27 @@ +--- +"effect": patch +--- + +Rename CLI constructors to PascalCase, aligning scalar names with `Schema` and `Config`. This is a breaking change; parsing behavior is unchanged. + +In `Primitive`, `Param`, `Flag`, and `Argument`, capitalize existing constructor names, with these exceptions: + +| Previous | New | Modules | +| --------- | ---------- | --------------------- | +| `integer` | `Int` | All four | +| `float` | `Finite` | All four | +| `none` | `Never` | All four | +| `choice` | `Literals` | Param, Flag, Argument | + +`Primitive.choice` becomes `Primitive.Choice`; `choiceWithValue` becomes `ChoiceWithValue` where available. + +In `Prompt`, capitalize control constructors except `text` → `String`, `integer` → `Int`, and `float` → `Number`. Rename public types `IntegerOptions` → `IntOptions` and `FloatOptions` → `NumberOptions`. Shared `TextOptions` is unchanged. `Prompt.Number` retains its existing parser, without a finite-number restriction. + +In `GlobalFlag`, rename `action` → `Action` and `setting` → `Setting`. Factories and combinators, including `Command.make` and `Prompt.succeed`, keep their names. + +Update public `_tag` matches and completion descriptors: + +- `Primitive`: `"Integer"` → `"Int"`, `"Float"` → `"Finite"`, `"None"` → `"Never"`. +- `Completions.FlagType` and `Completions.ArgumentType`: `"Integer"` → `"Int"`, `"Float"` → `"Finite"`. + +Sentinels still always fail; their internal parameter name is now `"__never__"`. Help labels and completion scripts are unchanged. diff --git a/repos/effect/.changeset/cli-variadic-absence-defaults.md b/repos/effect/.changeset/cli-variadic-absence-defaults.md new file mode 100644 index 0000000000..7f3f05e308 --- /dev/null +++ b/repos/effect/.changeset/cli-variadic-absence-defaults.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix defaulted variadic arguments when omitted. diff --git a/repos/effect/.changeset/cli-wizard-option-looking-values.md b/repos/effect/.changeset/cli-wizard-option-looking-values.md new file mode 100644 index 0000000000..852a593825 --- /dev/null +++ b/repos/effect/.changeset/cli-wizard-option-looking-values.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix CLI wizard handling of negative numbers and other flag values beginning with `-`. diff --git a/repos/effect/.changeset/cluster-active-teardowns.md b/repos/effect/.changeset/cluster-active-teardowns.md new file mode 100644 index 0000000000..c57eceae0e --- /dev/null +++ b/repos/effect/.changeset/cluster-active-teardowns.md @@ -0,0 +1,10 @@ +--- +"effect": patch +--- + +Cluster no longer retains fiber ids for every local teardown. + +Transient persisted interrupts are now classified from live teardown state +(entity, shard, singleton, entity type, and node shutdown) instead of a +process-lifetime set of fiber ids. The registry is bounded by in-flight +teardowns and returns to baseline after entity reap storms. diff --git a/repos/effect/.changeset/cluster-reply-codec-services.md b/repos/effect/.changeset/cluster-reply-codec-services.md new file mode 100644 index 0000000000..1ee7d1c8f2 --- /dev/null +++ b/repos/effect/.changeset/cluster-reply-codec-services.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Reply.Reply` codecs to require client services when decoding and server services when encoding. diff --git a/repos/effect/.changeset/cold-shards-continue.md b/repos/effect/.changeset/cold-shards-continue.md new file mode 100644 index 0000000000..e71308b84a --- /dev/null +++ b/repos/effect/.changeset/cold-shards-continue.md @@ -0,0 +1,15 @@ +--- +"effect": patch +--- + +Transient routing states for persisted cluster messages no longer surface as errors. + +If an entity moves runners or is shut down before replying, the caller keeps +waiting for the reply via message storage while the entity moves. If the local +runner is shutting down while a caller is waiting, the call is interrupted +instead of failing with `EntityNotAssignedToRunner`: the request is already +durable and will be served under the next owner. + +Durable workflows treat such an interrupt as an abandoned run attempt: the run +stops with nothing persisted, without running compensations or resuming the +parent, ready to replay on the replacement runner. diff --git a/repos/effect/.changeset/compatible-dependency-refresh.md b/repos/effect/.changeset/compatible-dependency-refresh.md new file mode 100644 index 0000000000..a2d211be45 --- /dev/null +++ b/repos/effect/.changeset/compatible-dependency-refresh.md @@ -0,0 +1,8 @@ +--- +"@effect/platform-node": patch +"@effect/sql-libsql": patch +"@effect/sql-mysql2": patch +"@effect/doctest": patch +--- + +Update dependencies to their latest compatible versions. diff --git a/repos/effect/.changeset/context-saved-getter-inference.md b/repos/effect/.changeset/context-saved-getter-inference.md new file mode 100644 index 0000000000..c3028f3605 --- /dev/null +++ b/repos/effect/.changeset/context-saved-getter-inference.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix saved curried `Context.get` calls incorrectly inferring their required service as `unknown`. diff --git a/repos/effect/.changeset/cookies-error-tag.md b/repos/effect/.changeset/cookies-error-tag.md new file mode 100644 index 0000000000..c523bca088 --- /dev/null +++ b/repos/effect/.changeset/cookies-error-tag.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the `CookiesError` tag in `effect/unstable/http` from `CookieError` to `CookiesError` to match the class name. diff --git a/repos/effect/.changeset/d1-raw-native-results.md b/repos/effect/.changeset/d1-raw-native-results.md new file mode 100644 index 0000000000..5441b86d1c --- /dev/null +++ b/repos/effect/.changeset/d1-raw-native-results.md @@ -0,0 +1,7 @@ +--- +"@effect/sql-d1": patch +--- + +Return the complete native `D1Result` from D1 statement `.raw`, preserving `success`, `meta`, and `results` instead of returning only the row array. + +Callers that treated `.raw` as an array should read `.results` or use an ordinary or `.unprepared` statement when only rows are needed. diff --git a/repos/effect/.changeset/datetime-calendar-parts.md b/repos/effect/.changeset/datetime-calendar-parts.md new file mode 100644 index 0000000000..5529fb6e21 --- /dev/null +++ b/repos/effect/.changeset/datetime-calendar-parts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Apply DateTime calendar parts without intermediate overflow. diff --git a/repos/effect/.changeset/deno-redis-url-credentials.md b/repos/effect/.changeset/deno-redis-url-credentials.md new file mode 100644 index 0000000000..c150c6fefb --- /dev/null +++ b/repos/effect/.changeset/deno-redis-url-credentials.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Decode percent-encoded Redis URL authority credentials before authentication. diff --git a/repos/effect/.changeset/deno-writefile-existing-mode.md b/repos/effect/.changeset/deno-writefile-existing-mode.md new file mode 100644 index 0000000000..17d44f9ed5 --- /dev/null +++ b/repos/effect/.changeset/deno-writefile-existing-mode.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Fix `FileSystem.writeFile` on Deno to preserve permissions when writing an existing file with an explicit mode. diff --git a/repos/effect/.changeset/docgen-alias-signature.md b/repos/effect/.changeset/docgen-alias-signature.md new file mode 100644 index 0000000000..58b06047fd --- /dev/null +++ b/repos/effect/.changeset/docgen-alias-signature.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": patch +--- + +Preserve alias names, constraints, and defaults in generated type signatures. diff --git a/repos/effect/.changeset/docgen-class-property-examples.md b/repos/effect/.changeset/docgen-class-property-examples.md new file mode 100644 index 0000000000..68bfef5408 --- /dev/null +++ b/repos/effect/.changeset/docgen-class-property-examples.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": patch +--- + +Type-check parsed class property examples and execute them when `runExamples` is enabled. Previously unchecked property examples with type errors now cause docgen to fail. diff --git a/repos/effect/.changeset/docgen-source-relative-module-pages.md b/repos/effect/.changeset/docgen-source-relative-module-pages.md new file mode 100644 index 0000000000..92efd26675 --- /dev/null +++ b/repos/effect/.changeset/docgen-source-relative-module-pages.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": patch +--- + +Fix module page paths for `.`, nested, and absolute source directories so modules with the same filename in different subdirectories produce distinct pages. diff --git a/repos/effect/.changeset/docgen-unique-example-files.md b/repos/effect/.changeset/docgen-unique-example-files.md new file mode 100644 index 0000000000..15a55b1362 --- /dev/null +++ b/repos/effect/.changeset/docgen-unique-example-files.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": patch +--- + +Prevent examples from modules with colliding flattened paths from overwriting each other, so all examples are checked and optionally executed. Temporary example filenames and diagnostic paths now include a numeric prefix. diff --git a/repos/effect/.changeset/doctest-markdown-typescript.md b/repos/effect/.changeset/doctest-markdown-typescript.md new file mode 100644 index 0000000000..a427190238 --- /dev/null +++ b/repos/effect/.changeset/doctest-markdown-typescript.md @@ -0,0 +1,5 @@ +--- +"@effect/doctest": patch +--- + +Fix spurious TypeScript syntax errors when running marked code fences in Markdown and MDX documents. diff --git a/repos/effect/.changeset/doctest-statement-boundary.md b/repos/effect/.changeset/doctest-statement-boundary.md new file mode 100644 index 0000000000..9e69e68ff4 --- /dev/null +++ b/repos/effect/.changeset/doctest-statement-boundary.md @@ -0,0 +1,5 @@ +--- +"@effect/doctest": patch +--- + +Preserve statement boundaries after generated doctest assertions. diff --git a/repos/effect/.changeset/dotenv-literal-substitution.md b/repos/effect/.changeset/dotenv-literal-substitution.md new file mode 100644 index 0000000000..0e351277d9 --- /dev/null +++ b/repos/effect/.changeset/dotenv-literal-substitution.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `ConfigProvider.fromDotEnvContents` variable expansion to preserve replacement tokens such as `$&` in referenced values. diff --git a/repos/effect/.changeset/durable-clock-zero-threshold.md b/repos/effect/.changeset/durable-clock-zero-threshold.md new file mode 100644 index 0000000000..3a23956d33 --- /dev/null +++ b/repos/effect/.changeset/durable-clock-zero-threshold.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `DurableClock.sleep` to preserve explicit `0` and `0n` in-memory thresholds. diff --git a/repos/effect/.changeset/durable-deferred-into-encoding-services.md b/repos/effect/.changeset/durable-deferred-into-encoding-services.md new file mode 100644 index 0000000000..6a823609a3 --- /dev/null +++ b/repos/effect/.changeset/durable-deferred-into-encoding-services.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Require schema encoding services when `DurableDeferred.into` records an exit. diff --git a/repos/effect/.changeset/dynamic-tool-parameter-schema.md b/repos/effect/.changeset/dynamic-tool-parameter-schema.md new file mode 100644 index 0000000000..7d16a999d2 --- /dev/null +++ b/repos/effect/.changeset/dynamic-tool-parameter-schema.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Update dynamic tools to advertise replacement parameter schemas after `setParameters`. diff --git a/repos/effect/.changeset/eff-1004-cli-display-width.md b/repos/effect/.changeset/eff-1004-cli-display-width.md new file mode 100644 index 0000000000..6c0fdf5938 --- /dev/null +++ b/repos/effect/.changeset/eff-1004-cli-display-width.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Align CLI help tables by terminal display width for wide, emoji, combining, and zero-width graphemes. diff --git a/repos/effect/.changeset/eff-1008-tool-param-failure-mode.md b/repos/effect/.changeset/eff-1008-tool-param-failure-mode.md new file mode 100644 index 0000000000..8939684f6c --- /dev/null +++ b/repos/effect/.changeset/eff-1008-tool-param-failure-mode.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/ai-anthropic": patch +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +"@effect/ai-openrouter": patch +--- + +Route tool call parameter validation failures through the tool's `failureMode` and drop `ToolParameterValidationError.toolParams`. diff --git a/repos/effect/.changeset/eff-1036-fiber-allocations.md b/repos/effect/.changeset/eff-1036-fiber-allocations.md new file mode 100644 index 0000000000..ddd8bbc1c1 --- /dev/null +++ b/repos/effect/.changeset/eff-1036-fiber-allocations.md @@ -0,0 +1,12 @@ +--- +"effect": patch +"@effect/opentelemetry": patch +--- + +Reduce memory usage in Effect primitives and fibers. + +Breaking: context-derived `Fiber` fields now live under `fiber.cache`. The +`currentScheduler`, `currentSpan`, `currentLogLevel`, `currentStackFrame`, and +`currentPreventYield` fields are now `scheduler`, `span`, `logLevel`, +`stackFrame`, and `preventYield`. Access `minimumLogLevel` and +`maxOpsBeforeYield` through `cache` as well. diff --git a/repos/effect/.changeset/eff-1038-http-server-allocations.md b/repos/effect/.changeset/eff-1038-http-server-allocations.md new file mode 100644 index 0000000000..26a8ea83bd --- /dev/null +++ b/repos/effect/.changeset/eff-1038-http-server-allocations.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/platform-node": patch +--- + +Reduce HTTP server allocation churn when tracing is not configured and for requests that complete synchronously. diff --git a/repos/effect/.changeset/eff-1039-rpc-server-allocations.md b/repos/effect/.changeset/eff-1039-rpc-server-allocations.md new file mode 100644 index 0000000000..5973fa3cf2 --- /dev/null +++ b/repos/effect/.changeset/eff-1039-rpc-server-allocations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reduce per-request RPC server allocations. diff --git a/repos/effect/.changeset/eff-1042-http-server-performance.md b/repos/effect/.changeset/eff-1042-http-server-performance.md new file mode 100644 index 0000000000..bbdd463230 --- /dev/null +++ b/repos/effect/.changeset/eff-1042-http-server-performance.md @@ -0,0 +1,10 @@ +--- +"effect": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +--- + +Improve HTTP server throughput by reducing routing, request handling, response +construction, and body encoding overhead. Add `Effect.withFiberSucceed` for +synchronously computing successful values from the current fiber. Copy pooled +byte views by their exact range when exposing `ArrayBuffer` values. diff --git a/repos/effect/.changeset/eff-1201-http-overhead.md b/repos/effect/.changeset/eff-1201-http-overhead.md new file mode 100644 index 0000000000..84500451c0 --- /dev/null +++ b/repos/effect/.changeset/eff-1201-http-overhead.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Reduce HTTP server overhead: complete freshly created header maps in place in +`HttpServerResponse.setHeader` and `setHeaders`, compare static route prefixes +with a prepared `startsWith`, map `HttpApi` schema errors eagerly for completed +decoder results, and implement `Effect.cached` as a dedicated one-time memo +without time-to-live machinery. diff --git a/repos/effect/.changeset/eff-854-pg-connection-startup.md b/repos/effect/.changeset/eff-854-pg-connection-startup.md new file mode 100644 index 0000000000..f9b351dd96 --- /dev/null +++ b/repos/effect/.changeset/eff-854-pg-connection-startup.md @@ -0,0 +1,19 @@ +--- +"@effect/sql-pg": patch +"effect": patch +--- + +Replace `@effect/sql-pg`'s `pg` runtime with a native PostgreSQL client. `PgConnection` and `PgPool` now handle connection setup, binary queries, prepared statements, pipelining, streaming, notifications, cancellation, and custom codecs. `PgConnection.listen` and `PgClient.listen` return scoped notification dequeues after PostgreSQL confirms the subscription. `PgClient` uses the native stack, and the legacy `fromPool`, `fromClient`, and `makeWith` constructors are removed. + +### Breaking changes + +- `fromPool`, `fromClient`, and `makeWith` are removed. Use `make` for a pool or `makeClient` for one connection. +- `PgClient.listen` returns a scoped `Effect, SqlError, Scope>` instead of a `Stream`. Acquisition completes after PostgreSQL confirms `LISTEN`, so notifications sent after it returns cannot be missed. +- `PgClientConfig.types` now accepts a `PgTypes.Registry` instead of `pg.CustomTypesConfig`. Plain object parameters are no longer inferred as JSON; wrap them with `sql.json`. +- Query strings must contain one statement. PostgreSQL's extended protocol rejects multi-statement strings. +- Results use the native binary codecs. In particular, `int8` decodes to `bigint`, `date` to a string, timestamps to Unix epoch milliseconds, and `bytea` or unknown OIDs to `Uint8Array`. `executeRaw` returns the native `PgConnection.Result` shape rather than `pg.Result`. +- Named prepared statements are enabled by default. Set `prepare: false` when using a pooler that cannot preserve prepared statements between queries. `Statement.unprepared` and `Statement.valuesUnprepared` use unnamed extended queries without adding entries to the prepared-statement cache. + +Inferred parameters stay permissive: strings bind untyped so the backend derives the type from the statement, and safe integers beyond the `int4` range bind as `int8`. + +Add `Pool.reserve` for exclusive access to a concurrent pool item, and fix waiter wakeups and capacity replacement after invalidation. diff --git a/repos/effect/.changeset/eff-961-node-socket-tls.md b/repos/effect/.changeset/eff-961-node-socket-tls.md new file mode 100644 index 0000000000..f00fe3a71a --- /dev/null +++ b/repos/effect/.changeset/eff-961-node-socket-tls.md @@ -0,0 +1,11 @@ +--- +"@effect/platform-node-shared": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +--- + +Add `NodeSocket.makeTls`, `NodeSocket.makeTlsChannel`, and `NodeSocket.layerTls` for TLS client connections. + +These mirror the existing `makeNet` family but dial `tls.connect`, so they take the full `tls.ConnectionOptions` set: +trust anchors (`ca`), client certificates (`cert` / `key`), ALPN protocols, and `servername`. The socket opens once the +handshake completes; a failed handshake fails with a `SocketOpenError`. diff --git a/repos/effect/.changeset/eff-963-node-socket-server-tls.md b/repos/effect/.changeset/eff-963-node-socket-server-tls.md new file mode 100644 index 0000000000..dfac557a2b --- /dev/null +++ b/repos/effect/.changeset/eff-963-node-socket-server-tls.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-node-shared": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +--- + +Add `NodeSocketServer.makeTls` and `NodeSocketServer.layerTls` for TLS socket servers. diff --git a/repos/effect/.changeset/eff-965-socket-upgrade.md b/repos/effect/.changeset/eff-965-socket-upgrade.md new file mode 100644 index 0000000000..019a3c48cf --- /dev/null +++ b/repos/effect/.changeset/eff-965-socket-upgrade.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/platform-node-shared": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +--- + +Add Socket.upgrade, for upgrading tcp sockets using STARTTLS diff --git a/repos/effect/.changeset/eff-969-dependency-updates.md b/repos/effect/.changeset/eff-969-dependency-updates.md new file mode 100644 index 0000000000..2d249ad2c9 --- /dev/null +++ b/repos/effect/.changeset/eff-969-dependency-updates.md @@ -0,0 +1,8 @@ +--- +"@effect/doctest": patch +"@effect/sql-d1": patch +"@effect/sql-mysql2": patch +"@effect/sql-pglite": patch +--- + +Update production dependencies to their latest releases. diff --git a/repos/effect/.changeset/eff-972-deno-socket-server.md b/repos/effect/.changeset/eff-972-deno-socket-server.md new file mode 100644 index 0000000000..87e1b20d5c --- /dev/null +++ b/repos/effect/.changeset/eff-972-deno-socket-server.md @@ -0,0 +1,11 @@ +--- +"@effect/platform-deno": patch +"@effect/platform-node-shared": patch +--- + +Use the node-shared socket server on Deno so accepted TCP connections support reader-scoped server TLS upgrades. + +### Breaking changes + +`DenoSocketServer.make` and `layer` now accept Node listen options. Use `host` instead of `hostname` for TCP and +`{ path }` instead of `{ transport: "unix", path }` for Unix sockets. Deno 2.8.3 or newer is now required. diff --git a/repos/effect/.changeset/eff-997-service-keys.md b/repos/effect/.changeset/eff-997-service-keys.md new file mode 100644 index 0000000000..8ba1ef8c6c --- /dev/null +++ b/repos/effect/.changeset/eff-997-service-keys.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize core service and runtime identities under their owning module namespaces. diff --git a/repos/effect/.changeset/effect-eager-transform-arguments.md b/repos/effect/.changeset/effect-eager-transform-arguments.md new file mode 100644 index 0000000000..05be690d22 --- /dev/null +++ b/repos/effect/.changeset/effect-eager-transform-arguments.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.fnUntracedEager` to pass the original function arguments to each transform after the current effect, matching `Effect.fn` and `Effect.fnUntraced`. diff --git a/repos/effect/.changeset/effect-scoped-service-restoration.md b/repos/effect/.changeset/effect-scoped-service-restoration.md new file mode 100644 index 0000000000..ffc7b79abf --- /dev/null +++ b/repos/effect/.changeset/effect-scoped-service-restoration.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.updateServiceScoped` cleanup when an inner service provider has already completed. Closing the scope now preserves the service's absence instead of failing with a missing-service defect. diff --git a/repos/effect/.changeset/effect-unmatched-reason-preservation.md b/repos/effect/.changeset/effect-unmatched-reason-preservation.md new file mode 100644 index 0000000000..38f3af546a --- /dev/null +++ b/repos/effect/.changeset/effect-unmatched-reason-preservation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve the original cause in `Effect.catchReason` and `Effect.catchReasons` when no nested reason matches and no fallback is provided. diff --git a/repos/effect/.changeset/effectable-class-override-delegation.md b/repos/effect/.changeset/effectable-class-override-delegation.md new file mode 100644 index 0000000000..9a99f8e882 --- /dev/null +++ b/repos/effect/.changeset/effectable-class-override-delegation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effectable.Class` evaluation by delegating to its abstract `asEffect()` method. The method is called on the instance for each execution, preserving current receiver state and provided services. diff --git a/repos/effect/.changeset/effectable-mixin.md b/repos/effect/.changeset/effectable-mixin.md new file mode 100644 index 0000000000..b087c5cde8 --- /dev/null +++ b/repos/effect/.changeset/effectable-mixin.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Effectable.Mixin` to insert the Effect prototype into an existing class inheritance chain. The returned abstract class requires an `asEffect` method and derives its Effect type from that method through polymorphic `this`. diff --git a/repos/effect/.changeset/effectify-mapper-input-tuples.md b/repos/effect/.changeset/effectify-mapper-input-tuples.md new file mode 100644 index 0000000000..9f64953080 --- /dev/null +++ b/repos/effect/.changeset/effectify-mapper-input-tuples.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the `onError` and `onSyncError` argument tuple types in `Effect.effectify` to include only caller inputs, excluding the synthesized callback. Mapper annotations that expected a callback slot must use the caller-input tuple instead. Runtime behavior is unchanged. diff --git a/repos/effect/.changeset/entity-proxy-client-codec-services.md b/repos/effect/.changeset/entity-proxy-client-codec-services.md new file mode 100644 index 0000000000..a7e883600c --- /dev/null +++ b/repos/effect/.changeset/entity-proxy-client-codec-services.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `EntityProxyServer` handler layers to include client-side codec service requirements. diff --git a/repos/effect/.changeset/entity-test-client-fatal-defect-option.md b/repos/effect/.changeset/entity-test-client-fatal-defect-option.md new file mode 100644 index 0000000000..403acc21d2 --- /dev/null +++ b/repos/effect/.changeset/entity-test-client-fatal-defect-option.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor the entity layer's `disableFatalDefects` option in `Entity.makeTestClient`. When enabled, a handler defect no longer fails other pending calls to the same entity ID. The failing call still reports its defect; omitted or false options retain fatal-defect behavior. diff --git a/repos/effect/.changeset/eventlog-authentication-forbidden-retry.md b/repos/effect/.changeset/eventlog-authentication-forbidden-retry.md new file mode 100644 index 0000000000..44b83c11b0 --- /dev/null +++ b/repos/effect/.changeset/eventlog-authentication-forbidden-retry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Retry `EventLogRemote` writes and change streams when authentication returns `Forbidden`. diff --git a/repos/effect/.changeset/eventlog-empty-chunk-framing.md b/repos/effect/.changeset/eventlog-empty-chunk-framing.md new file mode 100644 index 0000000000..c289354641 --- /dev/null +++ b/repos/effect/.changeset/eventlog-empty-chunk-framing.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Return one empty chunk when `ChunkedMessage.split` receives an empty `Uint8Array`. diff --git a/repos/effect/.changeset/exact-byte-size.md b/repos/effect/.changeset/exact-byte-size.md new file mode 100644 index 0000000000..d3f07145d4 --- /dev/null +++ b/repos/effect/.changeset/exact-byte-size.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +"@effect/platform-node-shared": patch +"@effect/platform-node": patch +--- + +Add `ByteSize` module and use it across the ecosystem diff --git a/repos/effect/.changeset/exact-http-file-body-length.md b/repos/effect/.changeset/exact-http-file-body-length.md new file mode 100644 index 0000000000..72c8cd6b8d --- /dev/null +++ b/repos/effect/.changeset/exact-http-file-body-length.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Calculate `HttpBody.file`, `HttpBody.fileFromInfo`, and `HttpClientRequest.bodyFile` content lengths with exact bigint arithmetic and EOF clamping. diff --git a/repos/effect/.changeset/execution-plan-captured-predicate.md b/repos/effect/.changeset/execution-plan-captured-predicate.md new file mode 100644 index 0000000000..5dbd29881c --- /dev/null +++ b/repos/effect/.changeset/execution-plan-captured-predicate.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `ExecutionPlan.captureRequirements` provides captured services to effectful `while` predicates. diff --git a/repos/effect/.changeset/fair-matches-infer.md b/repos/effect/.changeset/fair-matches-infer.md new file mode 100644 index 0000000000..8f7c4ab308 --- /dev/null +++ b/repos/effect/.changeset/fair-matches-infer.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix contextual typing for `Match` tag and discriminator handler maps when handlers use `Effect.fn` or `Effect.fnUntraced`. diff --git a/repos/effect/.changeset/fair-workflows-suspend.md b/repos/effect/.changeset/fair-workflows-suspend.md new file mode 100644 index 0000000000..0c9810dea0 --- /dev/null +++ b/repos/effect/.changeset/fair-workflows-suspend.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix parallel child workflows inside activities to dispatch before suspending, release activity resources during durable waits, and resume reliably when children complete during cleanup. diff --git a/repos/effect/.changeset/fetch-raw-stream-duplex.md b/repos/effect/.changeset/fetch-raw-stream-duplex.md new file mode 100644 index 0000000000..ebf4b44150 --- /dev/null +++ b/repos/effect/.changeset/fetch-raw-stream-duplex.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Set `duplex` for raw Web stream request bodies in `FetchHttpClient`. diff --git a/repos/effect/.changeset/fiber-map-reentrant-replacement.md b/repos/effect/.changeset/fiber-map-reentrant-replacement.md new file mode 100644 index 0000000000..6153f0ac2f --- /dev/null +++ b/repos/effect/.changeset/fiber-map-reentrant-replacement.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `FiberMap` losing track of fibers started under the same key by a replaced fiber's synchronous finalizer, ensuring they are interrupted when the map's scope closes. diff --git a/repos/effect/.changeset/fiber-registration-same-fiber.md b/repos/effect/.changeset/fiber-registration-same-fiber.md new file mode 100644 index 0000000000..ee7c0ef2cd --- /dev/null +++ b/repos/effect/.changeset/fiber-registration-same-fiber.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve an already registered fiber when `FiberHandle` or `FiberMap` registers it again with `onlyIfMissing: true`, instead of interrupting it and clearing the entry. diff --git a/repos/effect/.changeset/fifty-carrots-punch.md b/repos/effect/.changeset/fifty-carrots-punch.md new file mode 100644 index 0000000000..4ba43f52dc --- /dev/null +++ b/repos/effect/.changeset/fifty-carrots-punch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Exposed the platform specific pretty loggers separately. diff --git a/repos/effect/.changeset/filesystem-sink-undefined-flag.md b/repos/effect/.changeset/filesystem-sink-undefined-flag.md new file mode 100644 index 0000000000..4c26793e98 --- /dev/null +++ b/repos/effect/.changeset/filesystem-sink-undefined-flag.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `FileSystem.sink` to retain its default write flag when `flag` is undefined. diff --git a/repos/effect/.changeset/five-spoons-visit.md b/repos/effect/.changeset/five-spoons-visit.md new file mode 100644 index 0000000000..f3d0b9e51d --- /dev/null +++ b/repos/effect/.changeset/five-spoons-visit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Removed unused stderr option from Logger.consolePretty signature diff --git a/repos/effect/.changeset/fix-atom-http-stream-success-types.md b/repos/effect/.changeset/fix-atom-http-stream-success-types.md new file mode 100644 index 0000000000..186f81b120 --- /dev/null +++ b/repos/effect/.changeset/fix-atom-http-stream-success-types.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Match `AtomHttpApi` query and mutation success types to the generated HTTP client, +including SSE, binary streams, and header-wrapped responses. Stream transport, +decoding, and SSE errors now appear in the stream's error channel instead of +`never`, so code that assumed a failure-free stream may need to handle them. +Runtime and serialization behavior are unchanged. diff --git a/repos/effect/.changeset/fix-atom-http-top-level-dispatch.md b/repos/effect/.changeset/fix-atom-http-top-level-dispatch.md new file mode 100644 index 0000000000..d38505b77d --- /dev/null +++ b/repos/effect/.changeset/fix-atom-http-top-level-dispatch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `AtomHttpApi` query and mutation dispatch for top-level API groups. diff --git a/repos/effect/.changeset/fix-atom-query-zero-ttl.md b/repos/effect/.changeset/fix-atom-query-zero-ttl.md new file mode 100644 index 0000000000..f29a0e9951 --- /dev/null +++ b/repos/effect/.changeset/fix-atom-query-zero-ttl.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor explicit `timeToLive: 0` and `timeToLive: 0n` in `AtomRpc` and `AtomHttpApi` queries. Zero now opts out of the registry's default idle retention, matching other zero-duration inputs, so an unmounted query can be disposed and fetched again on remount. Omitting `timeToLive` still uses the registry default. diff --git a/repos/effect/.changeset/fix-atom-rpc-client-middleware-errors.md b/repos/effect/.changeset/fix-atom-rpc-client-middleware-errors.md new file mode 100644 index 0000000000..882cdf455a --- /dev/null +++ b/repos/effect/.changeset/fix-atom-rpc-client-middleware-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `AtomRpc` mutation and query atoms to include client middleware errors in their result error types. diff --git a/repos/effect/.changeset/fix-atom-writable-fallback.md b/repos/effect/.changeset/fix-atom-writable-fallback.md new file mode 100644 index 0000000000..711e5279ac --- /dev/null +++ b/repos/effect/.changeset/fix-atom-writable-fallback.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Forward `Atom.withFallback` writes to the primary atom. diff --git a/repos/effect/.changeset/fix-cors-vary.md b/repos/effect/.changeset/fix-cors-vary.md new file mode 100644 index 0000000000..b5c0fe58b4 --- /dev/null +++ b/repos/effect/.changeset/fix-cors-vary.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpMiddleware.cors` to preserve `Origin` and other required `Vary` dimensions. diff --git a/repos/effect/.changeset/fix-encoded-header-metadata.md b/repos/effect/.changeset/fix-encoded-header-metadata.md new file mode 100644 index 0000000000..d443806281 --- /dev/null +++ b/repos/effect/.changeset/fix-encoded-header-metadata.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use body status and encoding defaults in `HttpApiSchema.encodeToWithHeaders`. diff --git a/repos/effect/.changeset/fix-filesystem-integer-precision.md b/repos/effect/.changeset/fix-filesystem-integer-precision.md new file mode 100644 index 0000000000..70e684da55 --- /dev/null +++ b/repos/effect/.changeset/fix-filesystem-integer-precision.md @@ -0,0 +1,9 @@ +--- +"@effect/platform-node-shared": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +"effect": patch +--- + +Prevent precision loss in Node/Bun filesystem operations and `HttpPlatform` file responses. diff --git a/repos/effect/.changeset/fix-http-platform-file-ranges.md b/repos/effect/.changeset/fix-http-platform-file-ranges.md new file mode 100644 index 0000000000..87b522ab5f --- /dev/null +++ b/repos/effect/.changeset/fix-http-platform-file-ranges.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Clamp HttpPlatform file response ranges to the file size so Content-Length matches the bytes available. Oversized reads stop at EOF, and offsets at or past EOF return an empty body with Content-Length 0. Apply the same clamping to the default Web file response implementation. diff --git a/repos/effect/.changeset/fix-httpapi-form-responses.md b/repos/effect/.changeset/fix-httpapi-form-responses.md new file mode 100644 index 0000000000..9e9f6f3337 --- /dev/null +++ b/repos/effect/.changeset/fix-httpapi-form-responses.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApiClient` decoding form-urlencoded responses. diff --git a/repos/effect/.changeset/fix-httpapi-test-pre-response-handlers.md b/repos/effect/.changeset/fix-httpapi-test-pre-response-handlers.md new file mode 100644 index 0000000000..d1b3998517 --- /dev/null +++ b/repos/effect/.changeset/fix-httpapi-test-pre-response-handlers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Run registered pre-response handlers before `HttpApiTest` returns responses. diff --git a/repos/effect/.changeset/fix-httpapi-url-builder-base-path.md b/repos/effect/.changeset/fix-httpapi-url-builder-base-path.md new file mode 100644 index 0000000000..9733d4db0f --- /dev/null +++ b/repos/effect/.changeset/fix-httpapi-url-builder-base-path.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApiClient.urlBuilder` dropping base URL pathnames. diff --git a/repos/effect/.changeset/fix-json-schema-percent-references.md b/repos/effect/.changeset/fix-json-schema-percent-references.md new file mode 100644 index 0000000000..01c6a3c470 --- /dev/null +++ b/repos/effect/.changeset/fix-json-schema-percent-references.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `JsonPointer.parseUriFragment` and `JsonPointer.formatUriFragment` for converting RFC 6901 URI fragments, and use them to preserve percent-encoded definition names in exported JSON Schema references. JSON Schema compilation now rejects malformed local definition references returned by `toJsonSchema` hooks. Such hooks must percent-encode characters that URI fragments do not permit, for example `%` as `%25` and `#` as `%23`. diff --git a/repos/effect/.changeset/fix-mime-parameter-normalization.md b/repos/effect/.changeset/fix-mime-parameter-normalization.md new file mode 100644 index 0000000000..545607aac6 --- /dev/null +++ b/repos/effect/.changeset/fix-mime-parameter-normalization.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize MIME type parameters and whitespace in `Mime.getAllExtensions`. diff --git a/repos/effect/.changeset/fix-negative-file-seek.md b/repos/effect/.changeset/fix-negative-file-seek.md new file mode 100644 index 0000000000..50227499bf --- /dev/null +++ b/repos/effect/.changeset/fix-negative-file-seek.md @@ -0,0 +1,7 @@ +--- +"effect": patch +"@effect/platform-node-shared": patch +"@effect/platform-deno": patch +--- + +On Node and Deno, `FileSystem.File.seek` now rejects negative resulting positions with a `BadArgument` platform error, leaving the cursor unchanged. Its return type is now `Effect`. diff --git a/repos/effect/.changeset/fix-openapi-client-form-url-encoding.md b/repos/effect/.changeset/fix-openapi-client-form-url-encoding.md new file mode 100644 index 0000000000..99c7fd0343 --- /dev/null +++ b/repos/effect/.changeset/fix-openapi-client-form-url-encoding.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Encode form-urlencoded request bodies in generated HTTP clients. diff --git a/repos/effect/.changeset/fix-openapi-client-multipart-records.md b/repos/effect/.changeset/fix-openapi-client-multipart-records.md new file mode 100644 index 0000000000..c4c36d038d --- /dev/null +++ b/repos/effect/.changeset/fix-openapi-client-multipart-records.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Encode multipart record payloads as form data in generated schema-backed clients. diff --git a/repos/effect/.changeset/fix-openapi-endpoint-transform-order.md b/repos/effect/.changeset/fix-openapi-endpoint-transform-order.md new file mode 100644 index 0000000000..ff1335312d --- /dev/null +++ b/repos/effect/.changeset/fix-openapi-endpoint-transform-order.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Apply endpoint OpenAPI overrides and transforms after schema generation. diff --git a/repos/effect/.changeset/fix-openapi-stream-transform-client.md b/repos/effect/.changeset/fix-openapi-stream-transform-client.md new file mode 100644 index 0000000000..13f781b601 --- /dev/null +++ b/repos/effect/.changeset/fix-openapi-stream-transform-client.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Apply `transformClient` when generated binary and SSE streams are consumed, preserving lazy stream construction. diff --git a/repos/effect/.changeset/fix-prompt-date-tab-buffer.md b/repos/effect/.changeset/fix-prompt-date-tab-buffer.md new file mode 100644 index 0000000000..43f07df7a5 --- /dev/null +++ b/repos/effect/.changeset/fix-prompt-date-tab-buffer.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Prompt.date` carrying typed digits into the next field when pressing Tab, including when navigation wraps. diff --git a/repos/effect/.changeset/fix-react-ref-switch.md b/repos/effect/.changeset/fix-react-ref-switch.md new file mode 100644 index 0000000000..0044f52272 --- /dev/null +++ b/repos/effect/.changeset/fix-react-ref-switch.md @@ -0,0 +1,5 @@ +--- +"@effect/atom-react": patch +--- + +Ensure `useAtomRef` updates after switching refs when the new value matches the previous ref's value. diff --git a/repos/effect/.changeset/fix-reactivity-duplicate-keys.md b/repos/effect/.changeset/fix-reactivity-duplicate-keys.md new file mode 100644 index 0000000000..2f01110879 --- /dev/null +++ b/repos/effect/.changeset/fix-reactivity-duplicate-keys.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent `Reactivity.query` cleanup from failing when keys are repeated. diff --git a/repos/effect/.changeset/fix-read-alloc-invalid-sizes.md b/repos/effect/.changeset/fix-read-alloc-invalid-sizes.md new file mode 100644 index 0000000000..b656ca94c0 --- /dev/null +++ b/repos/effect/.changeset/fix-read-alloc-invalid-sizes.md @@ -0,0 +1,6 @@ +--- +"@effect/platform-node-shared": patch +"@effect/platform-deno": patch +--- + +Fix `File.readAlloc` on Node and Deno to fail with `PlatformError` (`BadArgument`) for negative, fractional, non-finite, or unallocatable sizes without moving the cursor. Zero-size reads continue to return `Option.none()` without moving the cursor. diff --git a/repos/effect/.changeset/fix-sink-flatmap-leftovers.md b/repos/effect/.changeset/fix-sink-flatmap-leftovers.md new file mode 100644 index 0000000000..0afe46e741 --- /dev/null +++ b/repos/effect/.changeset/fix-sink-flatmap-leftovers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve pending leftovers when a `Sink.flatMap` continuation completes without consuming input. diff --git a/repos/effect/.changeset/fix-sse-mixed-line-endings.md b/repos/effect/.changeset/fix-sse-mixed-line-endings.md new file mode 100644 index 0000000000..c1b2727290 --- /dev/null +++ b/repos/effect/.changeset/fix-sse-mixed-line-endings.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve SSE events with mixed line endings. diff --git a/repos/effect/.changeset/fix-static-head-range.md b/repos/effect/.changeset/fix-static-head-range.md new file mode 100644 index 0000000000..915c8deb81 --- /dev/null +++ b/repos/effect/.changeset/fix-static-head-range.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ignore Range headers on non-GET requests in HttpStaticServer. diff --git a/repos/effect/.changeset/fix-static-oversized-ranges.md b/repos/effect/.changeset/fix-static-oversized-ranges.md new file mode 100644 index 0000000000..5838914794 --- /dev/null +++ b/repos/effect/.changeset/fix-static-oversized-ranges.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Parse HttpStaticServer byte range integers exactly, including values above Number.MAX_SAFE_INTEGER. Oversized starts now return 416 with Content-Range instead of falling back to 200. Oversized ends clamp to the last byte, and oversized suffixes return the whole file as 206. diff --git a/repos/effect/.changeset/fix-stream-wrapper-status.md b/repos/effect/.changeset/fix-stream-wrapper-status.md new file mode 100644 index 0000000000..9568a5cfdc --- /dev/null +++ b/repos/effect/.changeset/fix-stream-wrapper-status.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApiBuilder` ignoring the status annotation on a `HttpApiSchema.WithHeaders` wrapper around a streaming success, which defected when the wrapper and inner statuses differed. diff --git a/repos/effect/.changeset/fix-toml-array-subtables.md b/repos/effect/.changeset/fix-toml-array-subtables.md new file mode 100644 index 0000000000..07d3edbae2 --- /dev/null +++ b/repos/effect/.changeset/fix-toml-array-subtables.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Toml.parse` rejecting child tables in separate array-of-tables entries. diff --git a/repos/effect/.changeset/fix-tool-result-serialization.md b/repos/effect/.changeset/fix-tool-result-serialization.md new file mode 100644 index 0000000000..65accd0f1b --- /dev/null +++ b/repos/effect/.changeset/fix-tool-result-serialization.md @@ -0,0 +1,13 @@ +--- +"effect": patch +--- + +Fix tool result serialization to select the codec using `isFailure` and preserve `encodedResult` through `Response.AllParts` round trips. + +Add `Tool.failureResultSchema(tool)` and `Tool.ExecutionFailure` to handle user failures, `AiError`, and denied or interrupted calls consistently. Also export `HttpRequestDetails` and `HttpResponseDetails` from `AiError`; the `Response` exports remain available. + +### Breaking changes + +- Stored results must match the selected schema. With success `Schema.Number` and failure `Schema.NumberFromString`, migrate failed results from `404` to `"404"`. +- `Response.ToolResultPart` returns `Schema.Codec` instead of `Schema.decodeTo`. Update annotations that depend on the old type. +- `Tool.FailureResult` and `Tool.Result`, including their encoded variants, now include `Tool.ExecutionFailure` in both failure modes. Handle it when narrowing failed results. diff --git a/repos/effect/.changeset/fix-vue-ref-switch.md b/repos/effect/.changeset/fix-vue-ref-switch.md new file mode 100644 index 0000000000..b804de834a --- /dev/null +++ b/repos/effect/.changeset/fix-vue-ref-switch.md @@ -0,0 +1,5 @@ +--- +"@effect/atom-vue": patch +--- + +Fix `useAtomRef` returning a stale value after switching refs. diff --git a/repos/effect/.changeset/fix-yaml-folded-scalars.md b/repos/effect/.changeset/fix-yaml-folded-scalars.md new file mode 100644 index 0000000000..eeb3dfef25 --- /dev/null +++ b/repos/effect/.changeset/fix-yaml-folded-scalars.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix folded YAML scalars to preserve paragraph and indentation breaks. diff --git a/repos/effect/.changeset/flat-pipelines-share.md b/repos/effect/.changeset/flat-pipelines-share.md new file mode 100644 index 0000000000..2716994723 --- /dev/null +++ b/repos/effect/.changeset/flat-pipelines-share.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Set the default `multiplexConcurrency` to 32 when PostgreSQL connection multiplexing is enabled. Set a lower value explicitly to limit how many statements share each connection. diff --git a/repos/effect/.changeset/formatter-defined-error-causes.md b/repos/effect/.changeset/formatter-defined-error-causes.md new file mode 100644 index 0000000000..51f66c0f44 --- /dev/null +++ b/repos/effect/.changeset/formatter-defined-error-causes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve defined falsy Error causes (`0`, `false`, `""`, `null`, `0n`, and `NaN`) in `Formatter.format` output. Missing and explicitly `undefined` causes remain omitted. diff --git a/repos/effect/.changeset/fresh-pools-rotate.md b/repos/effect/.changeset/fresh-pools-rotate.md new file mode 100644 index 0000000000..492f3ef413 --- /dev/null +++ b/repos/effect/.changeset/fresh-pools-rotate.md @@ -0,0 +1,6 @@ +--- +"@effect/sql-pg": patch +--- + +Allow each PostgreSQL pool connection to complete its first checkout before applying `connectionTTL`, so a zero TTL +disables connection reuse without entering an invalidate/reconnect loop. diff --git a/repos/effect/.changeset/fuzzy-classes-compare.md b/repos/effect/.changeset/fuzzy-classes-compare.md new file mode 100644 index 0000000000..eb54cb7f06 --- /dev/null +++ b/repos/effect/.changeset/fuzzy-classes-compare.md @@ -0,0 +1,8 @@ +--- +"effect": patch +--- + +Fix equivalence derivation for schema class APIs by adopting the equivalence of +their declared fields. Class declarations previously fell back to +`Equal.equals`, which also compared runtime properties outside the schema and +could make field-equivalent class instances compare as unequal. diff --git a/repos/effect/.changeset/graph-bellman-ford-infinite-cycle-barriers.md b/repos/effect/.changeset/graph-bellman-ford-infinite-cycle-barriers.md new file mode 100644 index 0000000000..e016e39031 --- /dev/null +++ b/repos/effect/.changeset/graph-bellman-ford-infinite-cycle-barriers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.bellmanFord` reporting a negative cycle as affecting a target across an impassable, positive-infinite-weight edge. Targets separated from the cycle by such edges now retain their finite shortest path or remain unreachable, while targets reachable from the cycle through finite-weight edges still report an error. diff --git a/repos/effect/.changeset/hashmap-collision-entries.md b/repos/effect/.changeset/hashmap-collision-entries.md new file mode 100644 index 0000000000..fa077d00bd --- /dev/null +++ b/repos/effect/.changeset/hashmap-collision-entries.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent `HashMap` iterators from exposing mutable internal collision entries. diff --git a/repos/effect/.changeset/hashring-exclusion-endpoint.md b/repos/effect/.changeset/hashring-exclusion-endpoint.md new file mode 100644 index 0000000000..3e54251e1e --- /dev/null +++ b/repos/effect/.changeset/hashring-exclusion-endpoint.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HashRing.getShards` skipping an eligible node at the first ring position when other nodes have reached their allocation quota. diff --git a/repos/effect/.changeset/headers-redacted-name-case.md b/repos/effect/.changeset/headers-redacted-name-case.md new file mode 100644 index 0000000000..70d6358da0 --- /dev/null +++ b/repos/effect/.changeset/headers-redacted-name-case.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Compare header names case-insensitively in `Headers.isRedactedName`. diff --git a/repos/effect/.changeset/headers-stateful-patterns.md b/repos/effect/.changeset/headers-stateful-patterns.md new file mode 100644 index 0000000000..db5ebef9ee --- /dev/null +++ b/repos/effect/.changeset/headers-stateful-patterns.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Headers.redact` and `Headers.isRedactedName` skipping matches when a redaction pattern is a global or sticky regular expression. diff --git a/repos/effect/.changeset/http-client-catch-response-result.md b/repos/effect/.changeset/http-client-catch-response-result.md new file mode 100644 index 0000000000..29bd409fb5 --- /dev/null +++ b/repos/effect/.changeset/http-client-catch-response-result.md @@ -0,0 +1,8 @@ +--- +"effect": patch +--- + +Constrain the data-first `HttpClient.catch(client, recover)` overload to recover with +`HttpClientResponse` values, matching the data-last overload. Callbacks returning other +success types are now rejected; use `Effect.catch` on the result of `client.execute(request)` +to recover to arbitrary values. diff --git a/repos/effect/.changeset/http-client-redirect-preprocessing-recovery.md b/repos/effect/.changeset/http-client-redirect-preprocessing-recovery.md new file mode 100644 index 0000000000..70c1b6e200 --- /dev/null +++ b/repos/effect/.changeset/http-client-redirect-preprocessing-recovery.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpClient.followRedirects` bypassing response-level recovery when request preprocessing fails. diff --git a/repos/effect/.changeset/http-head-stream-scope.md b/repos/effect/.changeset/http-head-stream-scope.md new file mode 100644 index 0000000000..93f35c1096 --- /dev/null +++ b/repos/effect/.changeset/http-head-stream-scope.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Close request scopes for streaming HEAD responses. diff --git a/repos/effect/.changeset/http-response-content-length.md b/repos/effect/.changeset/http-response-content-length.md new file mode 100644 index 0000000000..55326a9131 --- /dev/null +++ b/repos/effect/.changeset/http-response-content-length.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve `Content-Length` headers in `HttpServerResponse.fromWeb`. diff --git a/repos/effect/.changeset/http-router-normalized-prefix.md b/repos/effect/.changeset/http-router-normalized-prefix.md new file mode 100644 index 0000000000..a19f44155c --- /dev/null +++ b/repos/effect/.changeset/http-router-normalized-prefix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize router prefixes before removing them from handler request URLs. diff --git a/repos/effect/.changeset/http-runner-path-boundary.md b/repos/effect/.changeset/http-runner-path-boundary.md new file mode 100644 index 0000000000..412f194b10 --- /dev/null +++ b/repos/effect/.changeset/http-runner-path-boundary.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `HttpRunner` HTTP and WebSocket client URLs adding an extra leading slash to slash-prefixed paths. Insert the address/path separator only when it is missing, preserving intentional leading and interior slashes. + +This path correction is normally masked by router normalization, but prevents route misses for non-root paths when duplicate-slash normalization is disabled. Applications that compensate for the extra slash may need to remove that compensation. Router defaults and shared trailing-slash handling are unchanged. diff --git a/repos/effect/.changeset/http-web-handler-cold-start.md b/repos/effect/.changeset/http-web-handler-cold-start.md new file mode 100644 index 0000000000..5fb4aabd0b --- /dev/null +++ b/repos/effect/.changeset/http-web-handler-cold-start.md @@ -0,0 +1,8 @@ +--- +"effect": patch +--- + +Reduce cold start cost of `HttpRouter` and `HttpEffect` web handlers. + +- `HttpServerRespondable` no longer imports `Schema` to detect schema errors, which removes the Schema modules from bundles that do not otherwise use them (about 23% of a minimal `HttpRouter` bundle). +- `HttpRouter.toWebHandler`, `HttpEffect.toWebHandlerLayer` and `HttpEffect.toWebHandlerLayerWith` now build the layer immediately instead of on the first request. A failed build never surfaces as an unhandled rejection; every request rejects with the build error instead. diff --git a/repos/effect/.changeset/httpapi-middleware-error-dedupe.md b/repos/effect/.changeset/httpapi-middleware-error-dedupe.md new file mode 100644 index 0000000000..76d74654b3 --- /dev/null +++ b/repos/effect/.changeset/httpapi-middleware-error-dedupe.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApiMiddleware`-declared errors being duplicated and mis-encoded. diff --git a/repos/effect/.changeset/httpapi-sse-decode-options.md b/repos/effect/.changeset/httpapi-sse-decode-options.md new file mode 100644 index 0000000000..eb84e9032c --- /dev/null +++ b/repos/effect/.changeset/httpapi-sse-decode-options.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow generated `HttpApiClient` methods and `AtomHttpApi` queries and mutations to accept native SSE decode options per call through the request's `sseOptions` field. diff --git a/repos/effect/.changeset/indexeddb-out-of-line-primary-keys.md b/repos/effect/.changeset/indexeddb-out-of-line-primary-keys.md new file mode 100644 index 0000000000..30b66a4e5f --- /dev/null +++ b/repos/effect/.changeset/indexeddb-out-of-line-primary-keys.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Preserve out-of-line primary keys in IndexedDB query results, including secondary-index reads. diff --git a/repos/effect/.changeset/indexeddb-stream-query-limits.md b/repos/effect/.changeset/indexeddb-stream-query-limits.md new file mode 100644 index 0000000000..c125931656 --- /dev/null +++ b/repos/effect/.changeset/indexeddb-stream-query-limits.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Keep streamed IndexedDB selects within their query limits. diff --git a/repos/effect/.changeset/isolate-unencodable-hydration.md b/repos/effect/.changeset/isolate-unencodable-hydration.md new file mode 100644 index 0000000000..fa72c498e9 --- /dev/null +++ b/repos/effect/.changeset/isolate-unencodable-hydration.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent unencodable atom values from aborting dehydration of the rest of an atom registry. diff --git a/repos/effect/.changeset/large-postgres-messages.md b/repos/effect/.changeset/large-postgres-messages.md new file mode 100644 index 0000000000..a4fdd57f7a --- /dev/null +++ b/repos/effect/.changeset/large-postgres-messages.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Add a `maxMessageSize` connection option so PostgreSQL clients can receive backend messages larger than the 16 MiB default. diff --git a/repos/effect/.changeset/layer-error-observer-types.md b/repos/effect/.changeset/layer-error-observer-types.md new file mode 100644 index 0000000000..24bcdb3e2d --- /dev/null +++ b/repos/effect/.changeset/layer-error-observer-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Layer.tapError` and `Layer.tapCause` to require observers that accept the source layer's complete error type. diff --git a/repos/effect/.changeset/layer-span-trace-options.md b/repos/effect/.changeset/layer-span-trace-options.md new file mode 100644 index 0000000000..d0cb11a3b4 --- /dev/null +++ b/repos/effect/.changeset/layer-span-trace-options.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor `captureStackTrace` in both forms of `Layer.withSpan`. Layer construction diagnostics previously reported a location inside `Layer.ts` instead of the `withSpan` call site, and ignored `captureStackTrace: false` or a supplied lazy stack. diff --git a/repos/effect/.changeset/layermap-preloaded-acquisition-errors.md b/repos/effect/.changeset/layermap-preloaded-acquisition-errors.md new file mode 100644 index 0000000000..402864fb98 --- /dev/null +++ b/repos/effect/.changeset/layermap-preloaded-acquisition-errors.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Preserve resource acquisition errors on `LayerMap.Service` when `preload: true` is set. The yielded service instance and its `get`, `contextEffect`, and `contextEffectOption` accessors now retain the resource error type because a resource can fail when reacquired, even if preloading succeeded. + +Consumers that assumed these accessors had a `never` error must handle the resource error. Runtime behavior is unchanged. diff --git a/repos/effect/.changeset/lazy-bun-redis-import.md b/repos/effect/.changeset/lazy-bun-redis-import.md new file mode 100644 index 0000000000..9255d6d97f --- /dev/null +++ b/repos/effect/.changeset/lazy-bun-redis-import.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Load Bun's `RedisClient` lazily in `BunRedis`. diff --git a/repos/effect/.changeset/lazy-undici-loading.md b/repos/effect/.changeset/lazy-undici-loading.md new file mode 100644 index 0000000000..9c767e4ea9 --- /dev/null +++ b/repos/effect/.changeset/lazy-undici-loading.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Defer loading Undici until an Undici-backed layer is acquired, preventing Node HTTP client imports from replacing Node's global fetch dispatcher. Import Undici APIs from `@effect/platform-node/Undici` instead of the package root. diff --git a/repos/effect/.changeset/libsql-transaction-client-isolation.md b/repos/effect/.changeset/libsql-transaction-client-isolation.md new file mode 100644 index 0000000000..186ff4348e --- /dev/null +++ b/repos/effect/.changeset/libsql-transaction-client-isolation.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-libsql": patch +--- + +Isolate transaction contexts between separately created libSQL clients. diff --git a/repos/effect/.changeset/logger-complete-file-writes.md b/repos/effect/.changeset/logger-complete-file-writes.md new file mode 100644 index 0000000000..0c2c6983d7 --- /dev/null +++ b/repos/effect/.changeset/logger-complete-file-writes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Logger.toFile` dropping the remainder of a log batch when a successful file write writes only part of the buffer. File logging now uses the complete-write contract; write errors continue to be ignored. diff --git a/repos/effect/.changeset/mcp-http-resource-template-origins.md b/repos/effect/.changeset/mcp-http-resource-template-origins.md new file mode 100644 index 0000000000..b94ed9b0db --- /dev/null +++ b/repos/effect/.changeset/mcp-http-resource-template-origins.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `McpServer` HTTP resource templates failing to resolve. diff --git a/repos/effect/.changeset/mcp-prompt-decoded-parameter-types.md b/repos/effect/.changeset/mcp-prompt-decoded-parameter-types.md new file mode 100644 index 0000000000..180a6f1ad2 --- /dev/null +++ b/repos/effect/.changeset/mcp-prompt-decoded-parameter-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `McpServer.registerPrompt` callback types to use decoded prompt parameters. diff --git a/repos/effect/.changeset/mcp-structured-content-object.md b/repos/effect/.changeset/mcp-structured-content-object.md new file mode 100644 index 0000000000..d3d85d737f --- /dev/null +++ b/repos/effect/.changeset/mcp-structured-content-object.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +McpServer no longer sends `null` or array tool results as `structuredContent`, which MCP requires to be a JSON object. diff --git a/repos/effect/.changeset/memory-message-storage-clear-controls.md b/repos/effect/.changeset/memory-message-storage-clear-controls.md new file mode 100644 index 0000000000..d4f524b693 --- /dev/null +++ b/repos/effect/.changeset/memory-message-storage-clear-controls.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove queued control envelopes when clearing an address from in-memory message storage. diff --git a/repos/effect/.changeset/metric-attribute-order-identity.md b/repos/effect/.changeset/metric-attribute-order-identity.md new file mode 100644 index 0000000000..608c6d396e --- /dev/null +++ b/repos/effect/.changeset/metric-attribute-order-identity.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure metrics with equal attributes share a series regardless of attribute insertion order. diff --git a/repos/effect/.changeset/metric-registry-isolation.md b/repos/effect/.changeset/metric-registry-isolation.md new file mode 100644 index 0000000000..68aa520b5d --- /dev/null +++ b/repos/effect/.changeset/metric-registry-isolation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix metrics reused across different `MetricRegistry` services to read and update the active registry while preserving each registry's values when revisited. diff --git a/repos/effect/.changeset/model-field-option-undefined.md b/repos/effect/.changeset/model-field-option-undefined.md new file mode 100644 index 0000000000..3bca7a6913 --- /dev/null +++ b/repos/effect/.changeset/model-field-option-undefined.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Model.FieldOption` to preserve omitted variants. diff --git a/repos/effect/.changeset/mssql-binary-parameters.md b/repos/effect/.changeset/mssql-binary-parameters.md new file mode 100644 index 0000000000..9ceac54a6a --- /dev/null +++ b/repos/effect/.changeset/mssql-binary-parameters.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-mssql": patch +--- + +Fix automatic binary parameter binding in `@effect/sql-mssql`. diff --git a/repos/effect/.changeset/mssql-ntlm-domain.md b/repos/effect/.changeset/mssql-ntlm-domain.md new file mode 100644 index 0000000000..eaa55815f5 --- /dev/null +++ b/repos/effect/.changeset/mssql-ntlm-domain.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-mssql": patch +--- + +Forward the configured domain to the SQL Server driver so NTLM clients can be constructed with a supplied domain. diff --git a/repos/effect/.changeset/multipart-streamed-part-guard.md b/repos/effect/.changeset/multipart-streamed-part-guard.md new file mode 100644 index 0000000000..f593fc2240 --- /dev/null +++ b/repos/effect/.changeset/multipart-streamed-part-guard.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Multipart.isStreamPart` to recognize only a text `Field` or streamed `File`, while preserving `Multipart.isPart` for all branded multipart parts, including `PersistedFile` values. diff --git a/repos/effect/.changeset/mutable-arrays-preserve.md b/repos/effect/.changeset/mutable-arrays-preserve.md new file mode 100644 index 0000000000..17e37d48a4 --- /dev/null +++ b/repos/effect/.changeset/mutable-arrays-preserve.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schema.mutable` to preserve array and tuple metadata and reject node-level encodings. diff --git a/repos/effect/.changeset/mutable-list-bulk-prepend-tail.md b/repos/effect/.changeset/mutable-list-bulk-prepend-tail.md new file mode 100644 index 0000000000..61e8ed55a4 --- /dev/null +++ b/repos/effect/.changeset/mutable-list-bulk-prepend-tail.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve values added to an empty `MutableList` by `prependAll` when appending more values. diff --git a/repos/effect/.changeset/name-random-services.md b/repos/effect/.changeset/name-random-services.md new file mode 100644 index 0000000000..2fef9ba2e0 --- /dev/null +++ b/repos/effect/.changeset/name-random-services.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Export the `Random.Random` service interface and `Metric.MetricRegistry` type so custom service implementations can be annotated without accessing `Context.Reference` phantom types. diff --git a/repos/effect/.changeset/net-address-values.md b/repos/effect/.changeset/net-address-values.md new file mode 100644 index 0000000000..b6db60e642 --- /dev/null +++ b/repos/effect/.changeset/net-address-values.md @@ -0,0 +1,14 @@ +--- +"effect": patch +"@effect/sql-pg": patch +"@effect/platform-node": patch +"@effect/platform-node-shared": patch +"@effect/platform-deno": patch +"@effect/platform-bun": patch +--- + +Add `NetAddress` under `effect/unstable/net` for MAC, IP, internet socket, and Unix socket addresses, with checked parsing, schemas, equality, canonical string serialization, and URL formatting. Companion modules `IpInterface` and `IpNetwork` represent IP interfaces and CIDR networks. + +HTTP and socket servers now expose `NetAddress.SocketAddress`. Replace TCP `hostname` access with `NetAddress.formatIp(address.address)` and use `UnixPathAddress.path` for Unix sockets. URL helpers bracket IPv6 addresses and reject scoped IPv6. Bun and Deno HTTP server layers can now fail with `ServeError` when listener address conversion fails. + +PostgreSQL `inet` values now use `IpInterface`; `cidr` values use `IpNetwork` and reject addresses with host bits set. diff --git a/repos/effect/.changeset/ninety-books-sit.md b/repos/effect/.changeset/ninety-books-sit.md new file mode 100644 index 0000000000..8f347d0353 --- /dev/null +++ b/repos/effect/.changeset/ninety-books-sit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Terminate the `Stream.fromEventListener` stream after one item if `once: true`. diff --git a/repos/effect/.changeset/node-http-status-text.md b/repos/effect/.changeset/node-http-status-text.md new file mode 100644 index 0000000000..b83133d661 --- /dev/null +++ b/repos/effect/.changeset/node-http-status-text.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Forward custom and empty status text from NodeHttpServer responses. diff --git a/repos/effect/.changeset/node-response-preserve-bytes.md b/repos/effect/.changeset/node-response-preserve-bytes.md new file mode 100644 index 0000000000..add6daba82 --- /dev/null +++ b/repos/effect/.changeset/node-response-preserve-bytes.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Preserve response bytes when selecting the text reader in Node HTTP clients. diff --git a/repos/effect/.changeset/node-sink-cancel-drain.md b/repos/effect/.changeset/node-sink-cancel-drain.md new file mode 100644 index 0000000000..257aead2e6 --- /dev/null +++ b/repos/effect/.changeset/node-sink-cancel-drain.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Prevent Node sinks from submitting buffered writes after interruption while waiting for writable backpressure. diff --git a/repos/effect/.changeset/node-sink-finalization-errors.md b/repos/effect/.changeset/node-sink-finalization-errors.md new file mode 100644 index 0000000000..308442c81a --- /dev/null +++ b/repos/effect/.changeset/node-sink-finalization-errors.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Fix `NodeSink` hanging when a writable reports an error during finalization. diff --git a/repos/effect/.changeset/node-stream-buffer-size.md b/repos/effect/.changeset/node-stream-buffer-size.md new file mode 100644 index 0000000000..82b55372d8 --- /dev/null +++ b/repos/effect/.changeset/node-stream-buffer-size.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Remove the ignored `bufferSize` option from `NodeStream`. diff --git a/repos/effect/.changeset/node-watch-relative.md b/repos/effect/.changeset/node-watch-relative.md new file mode 100644 index 0000000000..792683eba5 --- /dev/null +++ b/repos/effect/.changeset/node-watch-relative.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Fix watch event classification for targets outside the current working directory. diff --git a/repos/effect/.changeset/node-worker-unsafe-send-envelope.md b/repos/effect/.changeset/node-worker-unsafe-send-envelope.md new file mode 100644 index 0000000000..999baa91a6 --- /dev/null +++ b/repos/effect/.changeset/node-worker-unsafe-send-envelope.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Preserve reply payloads sent through `NodeWorkerRunner.sendUnsafe`. diff --git a/repos/effect/.changeset/node-writeall-empty.md b/repos/effect/.changeset/node-writeall-empty.md new file mode 100644 index 0000000000..f503e0d709 --- /dev/null +++ b/repos/effect/.changeset/node-writeall-empty.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Allow `File.writeAll` to accept empty buffers. diff --git a/repos/effect/.changeset/normalize-collection-counts.md b/repos/effect/.changeset/normalize-collection-counts.md new file mode 100644 index 0000000000..878b667954 --- /dev/null +++ b/repos/effect/.changeset/normalize-collection-counts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize numeric collection and batch counts across `Stream`, `Channel`, `Sink`, `MutableList`, `RequestResolver`, `Queue`, `TxQueue`, `PubSub`, and `HashRing`, preventing fractional, `NaN`, and non-positive counts from producing incorrect output, exceptions, waits for the wrong batch size, or non-terminating pulls. diff --git a/repos/effect/.changeset/number-remainder-negative-zero-dividend.md b/repos/effect/.changeset/number-remainder-negative-zero-dividend.md new file mode 100644 index 0000000000..0544996962 --- /dev/null +++ b/repos/effect/.changeset/number-remainder-negative-zero-dividend.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Number.remainder` to preserve negative-zero dividends with ordinary finite divisors. diff --git a/repos/effect/.changeset/olive-queues-persist.md b/repos/effect/.changeset/olive-queues-persist.md new file mode 100644 index 0000000000..ee9e644cde --- /dev/null +++ b/repos/effect/.changeset/olive-queues-persist.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Improve `PersistedQueue` reliability across SQL, Redis, and memory stores. Retry policy now lives on `make()`, attempts count on claim, retries follow a `Schedule`, and exhausted or undecodable elements are dead-lettered. Add retention cleanup, durable acknowledgement retries, storage schema fixes, local poll wakeups, and fixes for the memory take race and Redis dedup growth. diff --git a/repos/effect/.changeset/openai-image-strings.md b/repos/effect/.changeset/openai-image-strings.md new file mode 100644 index 0000000000..5a8aae24ed --- /dev/null +++ b/repos/effect/.changeset/openai-image-strings.md @@ -0,0 +1,6 @@ +--- +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +--- + +Preserve image data supplied as strings in OpenAI Responses and OpenAI-compatible chat requests. diff --git a/repos/effect/.changeset/openai-optional-sequence-number.md b/repos/effect/.changeset/openai-optional-sequence-number.md new file mode 100644 index 0000000000..ffd414cd8f --- /dev/null +++ b/repos/effect/.changeset/openai-optional-sequence-number.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Allow OpenAI-compatible Responses API stream events to omit `sequence_number`. diff --git a/repos/effect/.changeset/openapi-omitted-additional-properties.md b/repos/effect/.changeset/openapi-omitted-additional-properties.md new file mode 100644 index 0000000000..4af8dc5ed3 --- /dev/null +++ b/repos/effect/.changeset/openapi-omitted-additional-properties.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Preserve omitted `additionalProperties` when generating schemas from OpenAPI documents. diff --git a/repos/effect/.changeset/openrouter-encrypted-tool-finish.md b/repos/effect/.changeset/openrouter-encrypted-tool-finish.md new file mode 100644 index 0000000000..4f5006ac1c --- /dev/null +++ b/repos/effect/.changeset/openrouter-encrypted-tool-finish.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openrouter": patch +--- + +Report `tool-calls` instead of `unknown` when an OpenRouter stream ends with `stop` after emitting tool calls and nonempty encrypted reasoning. diff --git a/repos/effect/.changeset/openrouter-strict-json-schema.md b/repos/effect/.changeset/openrouter-strict-json-schema.md new file mode 100644 index 0000000000..7366127595 --- /dev/null +++ b/repos/effect/.changeset/openrouter-strict-json-schema.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openrouter": patch +--- + +Exclude the provider-only `strictJsonSchema` option from outgoing OpenRouter request bodies while preserving structured-output and tool strictness settings. diff --git a/repos/effect/.changeset/opentelemetry-delta-interval-starts.md b/repos/effect/.changeset/opentelemetry-delta-interval-starts.md new file mode 100644 index 0000000000..c536f68231 --- /dev/null +++ b/repos/effect/.changeset/opentelemetry-delta-interval-starts.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Use collection interval starts for OpenTelemetry delta metrics. diff --git a/repos/effect/.changeset/optic-projection-replacement.md b/repos/effect/.changeset/optic-projection-replacement.md new file mode 100644 index 0000000000..3523631ae2 --- /dev/null +++ b/repos/effect/.changeset/optic-projection-replacement.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `Optic.pick` and `Optic.omit` delete focused optional fields omitted from a replacement. diff --git a/repos/effect/.changeset/optic-string-index-delete.md b/repos/effect/.changeset/optic-string-index-delete.md new file mode 100644 index 0000000000..8468eda4a7 --- /dev/null +++ b/repos/effect/.changeset/optic-string-index-delete.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Optic.optionalKey` to splice tuple elements selected by string indices. diff --git a/repos/effect/.changeset/order-consumed-criteria.md b/repos/effect/.changeset/order-consumed-criteria.md new file mode 100644 index 0000000000..85dd26e645 --- /dev/null +++ b/repos/effect/.changeset/order-consumed-criteria.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Order.combineAll` consuming one-shot iterables after the first comparison. diff --git a/repos/effect/.changeset/otlp-disabled-batch-flush.md b/repos/effect/.changeset/otlp-disabled-batch-flush.md new file mode 100644 index 0000000000..9ab34b05aa --- /dev/null +++ b/repos/effect/.changeset/otlp-disabled-batch-flush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix disabled OTLP batching to skip empty exports and avoid resending buffered items. diff --git a/repos/effect/.changeset/otlp-tracer-span-performance.md b/repos/effect/.changeset/otlp-tracer-span-performance.md new file mode 100644 index 0000000000..2319594b70 --- /dev/null +++ b/repos/effect/.changeset/otlp-tracer-span-performance.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Speed up `OtlpTracer` span creation and export. Spans now allocate identifiers, attributes, and events lazily, and `Encoding.randomHex` produces flat strings for 16 and 32 character identifiers so serialization no longer flattens ropes. diff --git a/repos/effect/.changeset/partitioned-semaphore-stale-cleanup.md b/repos/effect/.changeset/partitioned-semaphore-stale-cleanup.md new file mode 100644 index 0000000000..72b3fba030 --- /dev/null +++ b/repos/effect/.changeset/partitioned-semaphore-stale-cleanup.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `PartitionedSemaphore` leaving a new waiter suspended when a previously resumed waiter for the same partition is interrupted before its acquisition completes. diff --git a/repos/effect/.changeset/persisted-cache-lookup-throw.md b/repos/effect/.changeset/persisted-cache-lookup-throw.md new file mode 100644 index 0000000000..81b54be07e --- /dev/null +++ b/repos/effect/.changeset/persisted-cache-lookup-throw.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Persist synchronous defects thrown by `PersistedCache` lookups. diff --git a/repos/effect/.changeset/pg-explicit-ssl-precedence.md b/repos/effect/.changeset/pg-explicit-ssl-precedence.md new file mode 100644 index 0000000000..e52b6246fd --- /dev/null +++ b/repos/effect/.changeset/pg-explicit-ssl-precedence.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Honor explicit `ssl` options when PostgreSQL URLs use `sslmode=prefer` or `sslmode=allow`. diff --git a/repos/effect/.changeset/pglite-json-string-values.md b/repos/effect/.changeset/pglite-json-string-values.md new file mode 100644 index 0000000000..f30fa95214 --- /dev/null +++ b/repos/effect/.changeset/pglite-json-string-values.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pglite": patch +--- + +Preserve string values passed to `sql.json` when using PGlite. diff --git a/repos/effect/.changeset/pipeline-root-stdin.md b/repos/effect/.changeset/pipeline-root-stdin.md new file mode 100644 index 0000000000..1c380f8843 --- /dev/null +++ b/repos/effect/.changeset/pipeline-root-stdin.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Route pipeline handle input through the first process. diff --git a/repos/effect/.changeset/plain-tool-results.md b/repos/effect/.changeset/plain-tool-results.md new file mode 100644 index 0000000000..b58b402756 --- /dev/null +++ b/repos/effect/.changeset/plain-tool-results.md @@ -0,0 +1,7 @@ +--- +"@effect/ai-openai": patch +"@effect/ai-anthropic": patch +"@effect/ai-openrouter": patch +--- + +Preserve plain-text client tool results when encoding provider requests. diff --git a/repos/effect/.changeset/pool-preserve-reservations.md b/repos/effect/.changeset/pool-preserve-reservations.md new file mode 100644 index 0000000000..0f87426b27 --- /dev/null +++ b/repos/effect/.changeset/pool-preserve-reservations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep `Pool.reserve` items out of shared circulation when other borrowers return or overlapping reservations close. Restore available slots only after the last reservation closes. diff --git a/repos/effect/.changeset/port-http-api-builder-handler.md b/repos/effect/.changeset/port-http-api-builder-handler.md new file mode 100644 index 0000000000..a499e2fe28 --- /dev/null +++ b/repos/effect/.changeset/port-http-api-builder-handler.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Port `HttpApiBuilder.handler` from v3 to define reusable endpoint callbacks with inferred request, response, error, and service types. diff --git a/repos/effect/.changeset/postgres-channel-names.md b/repos/effect/.changeset/postgres-channel-names.md new file mode 100644 index 0000000000..7662aa95f9 --- /dev/null +++ b/repos/effect/.changeset/postgres-channel-names.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Reject PostgreSQL LISTEN and NOTIFY channel names longer than 63 UTF-8 bytes. diff --git a/repos/effect/.changeset/pre/cli-no-color-values.md b/repos/effect/.changeset/pre/cli-no-color-values.md new file mode 100644 index 0000000000..084859b631 --- /dev/null +++ b/repos/effect/.changeset/pre/cli-no-color-values.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Disable CLI formatter colors for every non-empty `NO_COLOR` value. diff --git a/repos/effect/.changeset/pre/proud-ears-say.md b/repos/effect/.changeset/pre/proud-ears-say.md index 01a09eae46..36b35a7bb5 100644 --- a/repos/effect/.changeset/pre/proud-ears-say.md +++ b/repos/effect/.changeset/pre/proud-ears-say.md @@ -3,4 +3,3 @@ --- Support standalone Effect.forEach data-last usage - diff --git a/repos/effect/.changeset/pre/thirty-forks-march.md b/repos/effect/.changeset/pre/thirty-forks-march.md index 0085d3d149..249bb675d1 100644 --- a/repos/effect/.changeset/pre/thirty-forks-march.md +++ b/repos/effect/.changeset/pre/thirty-forks-march.md @@ -3,4 +3,3 @@ --- Drop unreachable concurrency guard in iteratorEagerImpl - \ No newline at end of file diff --git a/repos/effect/.changeset/preserve-default-variant-class.md b/repos/effect/.changeset/preserve-default-variant-class.md new file mode 100644 index 0000000000..d263ec6aeb --- /dev/null +++ b/repos/effect/.changeset/preserve-default-variant-class.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve schema classes when extracting their default `VariantSchema` variant. diff --git a/repos/effect/.changeset/preserve-negative-counter-deltas.md b/repos/effect/.changeset/preserve-negative-counter-deltas.md new file mode 100644 index 0000000000..d8783c189d --- /dev/null +++ b/repos/effect/.changeset/preserve-negative-counter-deltas.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/opentelemetry": patch +--- + +Preserve negative counter deltas in OTLP and OpenTelemetry metric exports. diff --git a/repos/effect/.changeset/principled-collection-counts.md b/repos/effect/.changeset/principled-collection-counts.md new file mode 100644 index 0000000000..25bf6a2ef7 --- /dev/null +++ b/repos/effect/.changeset/principled-collection-counts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize numeric collection counts consistently across `Array`, `Chunk`, `Iterable`, and `String`, and make `TupleOf` fall back to `Array` for positive fractional lengths. diff --git a/repos/effect/.changeset/prompt-buffer-render.md b/repos/effect/.changeset/prompt-buffer-render.md new file mode 100644 index 0000000000..6cc05a9211 --- /dev/null +++ b/repos/effect/.changeset/prompt-buffer-render.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep the previous prompt frame visible until the next frame or submission is ready to display. diff --git a/repos/effect/.changeset/prompt-lossless-text-serialization.md b/repos/effect/.changeset/prompt-lossless-text-serialization.md new file mode 100644 index 0000000000..ead45aa747 --- /dev/null +++ b/repos/effect/.changeset/prompt-lossless-text-serialization.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve text parts and provider options when serializing prompts. diff --git a/repos/effect/.changeset/prompt-response-files.md b/repos/effect/.changeset/prompt-response-files.md new file mode 100644 index 0000000000..0f11a7d89c --- /dev/null +++ b/repos/effect/.changeset/prompt-response-files.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve generated files when converting AI responses to prompts. diff --git a/repos/effect/.changeset/pubsub-sliding-single-subscriber.md b/repos/effect/.changeset/pubsub-sliding-single-subscriber.md new file mode 100644 index 0000000000..3584a05098 --- /dev/null +++ b/repos/effect/.changeset/pubsub-sliding-single-subscriber.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix capacity-one PubSub subscriber cursors after sliding past messages, including duplicate delivery and invalid state when unsubscribing from a slid message. diff --git a/repos/effect/.changeset/pull-based-socket.md b/repos/effect/.changeset/pull-based-socket.md new file mode 100644 index 0000000000..98c762f59e --- /dev/null +++ b/repos/effect/.changeset/pull-based-socket.md @@ -0,0 +1,22 @@ +--- +"effect": patch +"@effect/platform-node-shared": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +"@effect/platform-browser": patch +"@effect/ai-openai": patch +--- + +Redesign `Socket` around a scoped, pull-based reader with transport backpressure. + +`Socket` now exposes `reader` and `writer`. Client reader acquisition dials and yields a pull of non-empty batches: one buffer for TCP and one entry per WebSocket frame. TCP applies backpressure while paused; pausable WebSockets pause at `highWaterMark` (64 KiB by default) and resume after draining. Browser WebSockets cannot pause, so they can fail with `SocketReadError` at a configured `highWaterMark`. Writes await native drain signals and batch with `cork` / `uncork` where available. + +### Breaking changes + +- `Socket.run`, `Socket.runString`, and `Socket.runRaw` are removed. Acquire `socket.reader` (or `Socket.readerBytes` / `Socket.readerString`) in a scope and pull in a loop. Code before the first pull replaces `onOpen`. +- `Socket.make` now takes `{ reader, writer }`. The writer acquisition is infallible and yields a `Writer` with `write` and `writeAll`; both operations can still fail with `SocketError`. +- Every close fails the pull with `SocketError` wrapping `SocketCloseError`. The close-code predicates are removed; use `Effect.retry` around the scoped read loop to reconnect. +- `Socket.toChannel` and `Socket.toChannelString` now read from the pull and fail on close. `Socket.toStream` is added for read-only consumption. +- `fromWebSocket` drops the `onInitialRun` option; `SendQueueCapacity` is removed. +- Accepted server sockets pause immediately. Their reader attaches to the existing connection and cannot reconnect after close. diff --git a/repos/effect/.changeset/queue-manual-flush.md b/repos/effect/.changeset/queue-manual-flush.md new file mode 100644 index 0000000000..c7f3fb4233 --- /dev/null +++ b/repos/effect/.changeset/queue-manual-flush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Queue.flush` and `Queue.flushUnsafe` for manually releasing pending takers, including after synchronous offers. diff --git a/repos/effect/.changeset/queue-reentrant-producers.md b/repos/effect/.changeset/queue-reentrant-producers.md new file mode 100644 index 0000000000..3048f717e2 --- /dev/null +++ b/repos/effect/.changeset/queue-reentrant-producers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Queue` message duplication, capacity overruns, and consumer defects when a resumed producer synchronously uses the same queue. Zero-capacity queues now reserve each handed-off message for its consumer before resuming the producer. diff --git a/repos/effect/.changeset/quiet-buffers-decode.md b/repos/effect/.changeset/quiet-buffers-decode.md new file mode 100644 index 0000000000..dbf009e185 --- /dev/null +++ b/repos/effect/.changeset/quiet-buffers-decode.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow `SqlEventJournal` to decode entry identifiers and payloads from SQL drivers that return BLOB values as `ArrayBuffer`. diff --git a/repos/effect/.changeset/quiet-buns-route.md b/repos/effect/.changeset/quiet-buns-route.md new file mode 100644 index 0000000000..e635008723 --- /dev/null +++ b/repos/effect/.changeset/quiet-buns-route.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Preserve configured Bun routes when Effect HTTP handlers are installed or restored. diff --git a/repos/effect/.changeset/quiet-graphs-simplify.md b/repos/effect/.changeset/quiet-graphs-simplify.md new file mode 100644 index 0000000000..cc52f4d7da --- /dev/null +++ b/repos/effect/.changeset/quiet-graphs-simplify.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the redundant `Graph.Proto` interface. Use `Graph.Graph` when accepting any immutable graph. diff --git a/repos/effect/.changeset/quiet-pandas-listen.md b/repos/effect/.changeset/quiet-pandas-listen.md new file mode 100644 index 0000000000..58aaa22f25 --- /dev/null +++ b/repos/effect/.changeset/quiet-pandas-listen.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pglite": patch +--- + +Align PGlite `listen` with the PostgreSQL client. It now returns a scoped dequeue after the listener is installed, providing an explicit readiness boundary and preserving notifications received before the first take. diff --git a/repos/effect/.changeset/quiet-streams-take.md b/repos/effect/.changeset/quiet-streams-take.md new file mode 100644 index 0000000000..c05bac54de --- /dev/null +++ b/repos/effect/.changeset/quiet-streams-take.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Treat `NaN` as a non-positive count in `Stream.take`. diff --git a/repos/effect/.changeset/random-exclusive-upper-endpoint.md b/repos/effect/.changeset/random-exclusive-upper-endpoint.md new file mode 100644 index 0000000000..ee55cc507e --- /dev/null +++ b/repos/effect/.changeset/random-exclusive-upper-endpoint.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Random.nextBetween` and `Crypto.randomBetween` returning their exclusive upper bound when floating-point arithmetic rounds up. diff --git a/repos/effect/.changeset/rate-limiter-reset-lifetime.md b/repos/effect/.changeset/rate-limiter-reset-lifetime.md new file mode 100644 index 0000000000..03547308d0 --- /dev/null +++ b/repos/effect/.changeset/rate-limiter-reset-lifetime.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Report the exact remaining store lifetime in `RateLimiter` fixed-window `resetAfter` metadata when `onExceeded` is `"delay"`, instead of rounding up to a whole window. Admission, returned delays, and remaining-token counts are unchanged. diff --git a/repos/effect/.changeset/rcmap-invalidation-release.md b/repos/effect/.changeset/rcmap-invalidation-release.md new file mode 100644 index 0000000000..c89b315aea --- /dev/null +++ b/repos/effect/.changeset/rcmap-invalidation-release.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `RcMap` and `LayerMap` cleanup after invalidating an actively borrowed entry and reacquiring the same key. +The invalidated resource is released when its last borrower closes, even with infinite idle TTL, without removing +the replacement entry. Old idle timers also leave replacement entries untouched. diff --git a/repos/effect/.changeset/rcmap-throwing-lookup.md b/repos/effect/.changeset/rcmap-throwing-lookup.md new file mode 100644 index 0000000000..2e8027ee59 --- /dev/null +++ b/repos/effect/.changeset/rcmap-throwing-lookup.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `RcMap` entries getting stuck when the lookup function throws synchronously. Later borrowers now receive the defect, and unused entries are released according to their idle TTL instead of permanently consuming capacity. diff --git a/repos/effect/.changeset/rcref-acquisition-shutdown.md b/repos/effect/.changeset/rcref-acquisition-shutdown.md new file mode 100644 index 0000000000..1ee8dedc28 --- /dev/null +++ b/repos/effect/.changeset/rcref-acquisition-shutdown.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep `RcRef` closed when an in-flight acquisition finishes after its owning scope has closed. Release the late-acquired resource and interrupt waiting borrowers instead of making the resource available again. diff --git a/repos/effect/.changeset/rcref-release-ownership.md b/repos/effect/.changeset/rcref-release-ownership.md new file mode 100644 index 0000000000..53bd9947b2 --- /dev/null +++ b/repos/effect/.changeset/rcref-release-ownership.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent `RcRef` borrower cleanup from discarding replacement resources after invalidation or reopening a reference after its owner scope has closed. diff --git a/repos/effect/.changeset/reason-annotate-context-only.md b/repos/effect/.changeset/reason-annotate-context-only.md new file mode 100644 index 0000000000..12b1b2c391 --- /dev/null +++ b/repos/effect/.changeset/reason-annotate-context-only.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Type `Cause.Reason#annotate` as accepting a `Context` only. diff --git a/repos/effect/.changeset/remove-msgpack.md b/repos/effect/.changeset/remove-msgpack.md new file mode 100644 index 0000000000..d7f457806f --- /dev/null +++ b/repos/effect/.changeset/remove-msgpack.md @@ -0,0 +1,8 @@ +--- +"@effect/platform-bun": patch +"@effect/platform-deno": patch +"@effect/platform-node": patch +"effect": patch +--- + +Remove the MessagePack encoding and RPC serialization APIs together with the `msgpackr` dependency. Event-log persistence and remote messages now use SchemaBinary, and cluster transports use SchemaBinary unless NDJSON is selected explicitly. diff --git a/repos/effect/.changeset/repeat-or-else-metadata-argument.md b/repos/effect/.changeset/repeat-or-else-metadata-argument.md new file mode 100644 index 0000000000..1e0828ffab --- /dev/null +++ b/repos/effect/.changeset/repeat-or-else-metadata-argument.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Correct the `Effect.repeatOrElse` fallback type to expose the previous step's `Schedule.Metadata`, matching the existing runtime value. diff --git a/repos/effect/.changeset/report-mcp-tool-failures.md b/repos/effect/.changeset/report-mcp-tool-failures.md new file mode 100644 index 0000000000..531baac797 --- /dev/null +++ b/repos/effect/.changeset/report-mcp-tool-failures.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Report recovered MCP toolkit failures and defects to configured `ErrorReporter`s, including declared tool failures returned with `isError: true`. diff --git a/repos/effect/.changeset/request-cache-cancellation.md b/repos/effect/.changeset/request-cache-cancellation.md new file mode 100644 index 0000000000..9feb720d78 --- /dev/null +++ b/repos/effect/.changeset/request-cache-cancellation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `RequestResolver.withCache` retaining abandoned entries when a pending request is cancelled diff --git a/repos/effect/.changeset/request-persisted-resolver-failures.md b/repos/effect/.changeset/request-persisted-resolver-failures.md new file mode 100644 index 0000000000..3268c26466 --- /dev/null +++ b/repos/effect/.changeset/request-persisted-resolver-failures.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve completed results and propagate resolver failures from `RequestResolver.persisted`. diff --git a/repos/effect/.changeset/request-race-cache.md b/repos/effect/.changeset/request-race-cache.md new file mode 100644 index 0000000000..2ae0bcc172 --- /dev/null +++ b/repos/effect/.changeset/request-race-cache.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep completed results in `RequestResolver.withCache` when a losing `RequestResolver.race` resolver is interrupted after the winner completes, avoiding repeated backend requests on subsequent equal lookups. diff --git a/repos/effect/.changeset/request-resolver-failure-causes.md b/repos/effect/.changeset/request-resolver-failure-causes.md new file mode 100644 index 0000000000..a77da246ad --- /dev/null +++ b/repos/effect/.changeset/request-resolver-failure-causes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve typed errors, defects, and interrupts from `RequestResolver.fromEffectTagged` handlers. diff --git a/repos/effect/.changeset/request-resolver-iterable-results.md b/repos/effect/.changeset/request-resolver-iterable-results.md new file mode 100644 index 0000000000..59f70109ea --- /dev/null +++ b/repos/effect/.changeset/request-resolver-iterable-results.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `RequestResolver.fromEffectTagged` to consume handler results as an iterable, allowing arrays, iterators, and generators to resolve requests in order. diff --git a/repos/effect/.changeset/required-keys-index-signatures.md b/repos/effect/.changeset/required-keys-index-signatures.md new file mode 100644 index 0000000000..61a9a583db --- /dev/null +++ b/repos/effect/.changeset/required-keys-index-signatures.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Types.RequiredKeys` dropping named required keys on types with index signatures. Derived type annotations may need to include these keys. diff --git a/repos/effect/.changeset/resolved-client-urls.md b/repos/effect/.changeset/resolved-client-urls.md new file mode 100644 index 0000000000..b3554d5300 --- /dev/null +++ b/repos/effect/.changeset/resolved-client-urls.md @@ -0,0 +1,8 @@ +--- +"@effect/platform-browser": patch +"@effect/platform-node": patch +"effect": patch +--- + +Add `HttpClientResponse.url`, including query parameters and excluding the hash. When redirects are followed, it reports +the final URL. diff --git a/repos/effect/.changeset/rpc-falsy-control-ids.md b/repos/effect/.changeset/rpc-falsy-control-ids.md new file mode 100644 index 0000000000..0c2d815d79 --- /dev/null +++ b/repos/effect/.changeset/rpc-falsy-control-ids.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve zero and empty-string request IDs in JSON-RPC control messages. diff --git a/repos/effect/.changeset/scheduler-global-scope-timers.md b/repos/effect/.changeset/scheduler-global-scope-timers.md new file mode 100644 index 0000000000..af24f0d771 --- /dev/null +++ b/repos/effect/.changeset/scheduler-global-scope-timers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +The default scheduler falls back to a microtask when setting a timer throws. Cloudflare Workers disallow timers in global scope, so an effect that yielded while running at module load failed with "Disallowed operation called within global scope". diff --git a/repos/effect/.changeset/schema-api-cleanup.md b/repos/effect/.changeset/schema-api-cleanup.md new file mode 100644 index 0000000000..b7a4c3d727 --- /dev/null +++ b/repos/effect/.changeset/schema-api-cleanup.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Move the built-in schema revivers from `Schema` to `SchemaRepresentation`. +Rename the reviver constructors to `makeReviverDeclaration`, +`makeReviverFilter`, and `makeReviverFilterGroup`. + +Change `Schema.toEncoderXml` to fail with `SchemaIssue.Issue` directly instead +of wrapping failures in `SchemaError`. Consumers that read `error.issue` should +now use the error value itself. diff --git a/repos/effect/.changeset/schema-array-leaf-aggregation.md b/repos/effect/.changeset/schema-array-leaf-aggregation.md new file mode 100644 index 0000000000..daf0561a86 --- /dev/null +++ b/repos/effect/.changeset/schema-array-leaf-aggregation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve array-valued leaves when `SchemaGetter.makeTreeRecord` aggregates duplicate paths. diff --git a/repos/effect/.changeset/schema-binary-preserve-bom.md b/repos/effect/.changeset/schema-binary-preserve-bom.md new file mode 100644 index 0000000000..2248164f13 --- /dev/null +++ b/repos/effect/.changeset/schema-binary-preserve-bom.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve leading U+FEFF characters in SchemaBinary string values when decoding. diff --git a/repos/effect/.changeset/scoped-cache-invalidate-all-reentrancy.md b/repos/effect/.changeset/scoped-cache-invalidate-all-reentrancy.md new file mode 100644 index 0000000000..2f7bc048a5 --- /dev/null +++ b/repos/effect/.changeset/scoped-cache-invalidate-all-reentrancy.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `ScopedCache.invalidateAll` discarding entries created by reentrant resource finalizers without releasing them. Entries are now removed before their finalizers run, so replacement resources remain cached and are released when the cache closes. diff --git a/repos/effect/.changeset/scoped-cache-refresh-lookup-defects.md b/repos/effect/.changeset/scoped-cache-refresh-lookup-defects.md new file mode 100644 index 0000000000..412dc00d9c --- /dev/null +++ b/repos/effect/.changeset/scoped-cache-refresh-lookup-defects.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Capture synchronous defects thrown by `ScopedCache.refresh` lookup callbacks. diff --git a/repos/effect/.changeset/scoped-log-nan-restoration.md b/repos/effect/.changeset/scoped-log-nan-restoration.md new file mode 100644 index 0000000000..07413320b8 --- /dev/null +++ b/repos/effect/.changeset/scoped-log-nan-restoration.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.annotateLogsScoped` to restore or remove unchanged `NaN` annotations when the scope closes. diff --git a/repos/effect/.changeset/shaky-terms-push.md b/repos/effect/.changeset/shaky-terms-push.md new file mode 100644 index 0000000000..9e004ee1e5 --- /dev/null +++ b/repos/effect/.changeset/shaky-terms-push.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix HttpRouter nested prefixed application order diff --git a/repos/effect/.changeset/sharding-registration-context-overrides.md b/repos/effect/.changeset/sharding-registration-context-overrides.md new file mode 100644 index 0000000000..67c74a7d12 --- /dev/null +++ b/repos/effect/.changeset/sharding-registration-context-overrides.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor services explicitly supplied when registering cluster entities while retaining construction-context services as fallbacks. diff --git a/repos/effect/.changeset/shy-seals-smile.md b/repos/effect/.changeset/shy-seals-smile.md new file mode 100644 index 0000000000..ac3d45baae --- /dev/null +++ b/repos/effect/.changeset/shy-seals-smile.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-react-native": patch +--- + +Return selected rows from synchronous and asynchronous OP-SQLite value queries. diff --git a/repos/effect/.changeset/skip-disabled-stack-capture.md b/repos/effect/.changeset/skip-disabled-stack-capture.md new file mode 100644 index 0000000000..cbdf29e4f7 --- /dev/null +++ b/repos/effect/.changeset/skip-disabled-stack-capture.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Skip optional stack capture when `Error.stackTraceLimit` is zero. diff --git a/repos/effect/.changeset/small-basic-bundle.md b/repos/effect/.changeset/small-basic-bundle.md new file mode 100644 index 0000000000..eef21766d8 --- /dev/null +++ b/repos/effect/.changeset/small-basic-bundle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reduce the basic Effect bundle size by keeping cause deduplication local, making encoding lookup tables tree-shakeable, removing redundant cause field declarations, and simplifying primitive hash dispatch without changing hash values. diff --git a/repos/effect/.changeset/socket-paused-websocket-handoff.md b/repos/effect/.changeset/socket-paused-websocket-handoff.md new file mode 100644 index 0000000000..b8ee1aaef1 --- /dev/null +++ b/repos/effect/.changeset/socket-paused-websocket-handoff.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Resume paused WebSockets after their readers take ownership. diff --git a/repos/effect/.changeset/socket-websocket-text-byte-watermark.md b/repos/effect/.changeset/socket-websocket-text-byte-watermark.md new file mode 100644 index 0000000000..bd35e74b5a --- /dev/null +++ b/repos/effect/.changeset/socket-websocket-text-byte-watermark.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Count buffered WebSocket text frames by their UTF-8 byte length when enforcing `highWaterMark`. diff --git a/repos/effect/.changeset/split-lines-completed-cr.md b/repos/effect/.changeset/split-lines-completed-cr.md new file mode 100644 index 0000000000..5a3608f587 --- /dev/null +++ b/repos/effect/.changeset/split-lines-completed-cr.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Emit CR-terminated lines from `Stream.splitLines` without pulling upstream again. diff --git a/repos/effect/.changeset/spotty-masks-own.md b/repos/effect/.changeset/spotty-masks-own.md new file mode 100644 index 0000000000..16f4feff02 --- /dev/null +++ b/repos/effect/.changeset/spotty-masks-own.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Loosen Stream.addEventListener type parameter diff --git a/repos/effect/.changeset/sql-event-journal-callback-errors.md b/repos/effect/.changeset/sql-event-journal-callback-errors.md new file mode 100644 index 0000000000..2d30161755 --- /dev/null +++ b/repos/effect/.changeset/sql-event-journal-callback-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve callback error identity in `SqlEventJournal.write` and `SqlEventJournal.withRemoteUncommited`. diff --git a/repos/effect/.changeset/sql-message-storage-joined-reply-id.md b/repos/effect/.changeset/sql-message-storage-joined-reply-id.md new file mode 100644 index 0000000000..37a3db749f --- /dev/null +++ b/repos/effect/.changeset/sql-message-storage-joined-reply-id.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve reply IDs in SQL-backed `MessageStorage.unprocessedMessagesById` reads. diff --git a/repos/effect/.changeset/sql-nested-placeholder-cache.md b/repos/effect/.changeset/sql-nested-placeholder-cache.md new file mode 100644 index 0000000000..9788260519 --- /dev/null +++ b/repos/effect/.changeset/sql-nested-placeholder-cache.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix placeholder numbering for cached fragments used in returning helpers. diff --git a/repos/effect/.changeset/sql-optional-span-propagation.md b/repos/effect/.changeset/sql-optional-span-propagation.md new file mode 100644 index 0000000000..f925c1ad08 --- /dev/null +++ b/repos/effect/.changeset/sql-optional-span-propagation.md @@ -0,0 +1,12 @@ +--- +"effect": patch +--- + +Add `Statement.SpanPropagationEnabled` to scope driver span parenting under `sql.execute` for any SQL client. Disabled by default. + +```ts +import { Effect } from "effect" +import { Statement } from "effect/unstable/sql" + +query.pipe(Effect.provideService(Statement.SpanPropagationEnabled, true)) +``` diff --git a/repos/effect/.changeset/sql-returning-identifier.md b/repos/effect/.changeset/sql-returning-identifier.md new file mode 100644 index 0000000000..7922dc865d --- /dev/null +++ b/repos/effect/.changeset/sql-returning-identifier.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix SQL returning helpers to compile identifiers with dialect-specific escaping. diff --git a/repos/effect/.changeset/sql-runner-requested-shard-results.md b/repos/effect/.changeset/sql-runner-requested-shard-results.md new file mode 100644 index 0000000000..bbce8efe4d --- /dev/null +++ b/repos/effect/.changeset/sql-runner-requested-shard-results.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure PostgreSQL shard acquisition and refresh return only the requested shards. diff --git a/repos/effect/.changeset/sqlite-bun-transaction-export.md b/repos/effect/.changeset/sqlite-bun-transaction-export.md new file mode 100644 index 0000000000..52739714a6 --- /dev/null +++ b/repos/effect/.changeset/sqlite-bun-transaction-export.md @@ -0,0 +1,6 @@ +--- +"@effect/sql-sqlite-bun": patch +--- + +Allow database exports inside `withTransaction` to complete instead of waiting indefinitely. The exported snapshot +includes the transaction's uncommitted writes, while exports outside that transaction still wait for it to finish. diff --git a/repos/effect/.changeset/sqlite-do-stream-errors.md b/repos/effect/.changeset/sqlite-do-stream-errors.md new file mode 100644 index 0000000000..af0b108b00 --- /dev/null +++ b/repos/effect/.changeset/sqlite-do-stream-errors.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-do": patch +--- + +Report synchronous Durable Object SQL streaming failures as recoverable `SqlError` values. diff --git a/repos/effect/.changeset/sqlite-node-unprepared-errors.md b/repos/effect/.changeset/sqlite-node-unprepared-errors.md new file mode 100644 index 0000000000..be4ae32fa6 --- /dev/null +++ b/repos/effect/.changeset/sqlite-node-unprepared-errors.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-node": patch +--- + +Capture unprepared statement preparation failures as typed `SqlError`s. diff --git a/repos/effect/.changeset/sqlite-wasm-messageport-startup.md b/repos/effect/.changeset/sqlite-wasm-messageport-startup.md new file mode 100644 index 0000000000..bd2530e4c5 --- /dev/null +++ b/repos/effect/.changeset/sqlite-wasm-messageport-startup.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-wasm": patch +--- + +Start incoming MessagePort queues after registering the SQLite WASM client and OPFS worker listeners. Fresh ports, including a SharedWorker's client port, no longer require manual activation to receive ready messages and query replies or process worker requests. diff --git a/repos/effect/.changeset/sqlite-wasm-statement-columns.md b/repos/effect/.changeset/sqlite-wasm-statement-columns.md new file mode 100644 index 0000000000..889b6cc7da --- /dev/null +++ b/repos/effect/.changeset/sqlite-wasm-statement-columns.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-wasm": patch +--- + +Preserve statement-specific column names in SQLite WASM worker-backed query results. diff --git a/repos/effect/.changeset/sqlite-wasm-worker-error-metadata.md b/repos/effect/.changeset/sqlite-wasm-worker-error-metadata.md new file mode 100644 index 0000000000..b4e0486ccc --- /dev/null +++ b/repos/effect/.changeset/sqlite-wasm-worker-error-metadata.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-wasm": patch +--- + +Preserve native SQLite error codes in OPFS worker replies so the client can classify constraint failures as `ConstraintError` instead of `UnknownError`. For coded worker failures, `SqlError.reason.cause` now contains a `{ message, code }` record instead of a string. diff --git a/repos/effect/.changeset/sqlmodel-insert-decoding-services.md b/repos/effect/.changeset/sqlmodel-insert-decoding-services.md new file mode 100644 index 0000000000..58b1a15a3d --- /dev/null +++ b/repos/effect/.changeset/sqlmodel-insert-decoding-services.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Include the model's decoding services in the public requirements of `SqlModel.makeResolvers().insert`, alongside its existing input-encoding services. + +This intentionally tightens compile-time checking: previously accepted callers must now provide the services already needed to decode inserted rows at runtime. Provide those services when executing the insert with `SqlResolver.request`. `insertVoid` still requires only input-encoding services, and service-free models need no changes. Runtime behavior is unchanged. diff --git a/repos/effect/.changeset/stream-rechunk-large-source.md b/repos/effect/.changeset/stream-rechunk-large-source.md new file mode 100644 index 0000000000..f96dfb7d31 --- /dev/null +++ b/repos/effect/.changeset/stream-rechunk-large-source.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.rechunk` failing on large source chunks. diff --git a/repos/effect/.changeset/strict-content-length.md b/repos/effect/.changeset/strict-content-length.md new file mode 100644 index 0000000000..f4fcb06141 --- /dev/null +++ b/repos/effect/.changeset/strict-content-length.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Parse `Content-Length` metadata strictly across HTTP modules, ignoring malformed or unsafe values instead of coercing them. diff --git a/repos/effect/.changeset/strict-cookie-names.md b/repos/effect/.changeset/strict-cookie-names.md new file mode 100644 index 0000000000..4795f4d71a --- /dev/null +++ b/repos/effect/.changeset/strict-cookie-names.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ignore `Set-Cookie` headers whose cookie names do not satisfy the RFC 6265 token syntax. diff --git a/repos/effect/.changeset/struct-numeric-selection.md b/repos/effect/.changeset/struct-numeric-selection.md new file mode 100644 index 0000000000..7a947d2afd --- /dev/null +++ b/repos/effect/.changeset/struct-numeric-selection.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor numeric property selectors in Struct selection and mapping utilities. diff --git a/repos/effect/.changeset/structural-schema-nodes.md b/repos/effect/.changeset/structural-schema-nodes.md new file mode 100644 index 0000000000..41d9897d24 --- /dev/null +++ b/repos/effect/.changeset/structural-schema-nodes.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Expose `SchemaAST` nodes, `SchemaIssue` nodes, `SchemaGetter.Getter`, and the `SchemaTransformation` models through structural instance interfaces instead of concrete class declarations. The constructors remain usable with `new` and `instanceof`, but their `prototype` is no longer part of the public TypeScript API. Replace type-level access through a constructor's `prototype` with the corresponding named instance interface, such as `SchemaGetter.Getter`. + +`SchemaAST.Base` is no longer exported. Use `SchemaAST.AST` when accepting any AST node, and use the `SchemaAST.is*` guards to narrow individual variants. diff --git a/repos/effect/.changeset/synchronized-ref-modify-some-effect-currying.md b/repos/effect/.changeset/synchronized-ref-modify-some-effect-currying.md new file mode 100644 index 0000000000..8691cfd97a --- /dev/null +++ b/repos/effect/.changeset/synchronized-ref-modify-some-effect-currying.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the curried `SynchronizedRef.modifySomeEffect` overload to accept only the callback, matching its runtime behavior. diff --git a/repos/effect/.changeset/synchronized-ref-not-ref.md b/repos/effect/.changeset/synchronized-ref-not-ref.md new file mode 100644 index 0000000000..13bee8c918 --- /dev/null +++ b/repos/effect/.changeset/synchronized-ref-not-ref.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Stop declaring `SynchronizedRef` as a subtype of `Ref`, preventing `Ref` combinators from accepting values that do not implement the required runtime representation. diff --git a/repos/effect/.changeset/tap-defect-saved-error-inference.md b/repos/effect/.changeset/tap-defect-saved-error-inference.md new file mode 100644 index 0000000000..9c85a3983a --- /dev/null +++ b/repos/effect/.changeset/tap-defect-saved-error-inference.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve the source error type when a saved `Effect.tapDefect` operator is applied. The source error is now inferred from each application instead of when the operator is created. Runtime behavior is unchanged. diff --git a/repos/effect/.changeset/tcp-schema-binary-default.md b/repos/effect/.changeset/tcp-schema-binary-default.md new file mode 100644 index 0000000000..d0a2e0072f --- /dev/null +++ b/repos/effect/.changeset/tcp-schema-binary-default.md @@ -0,0 +1,12 @@ +--- +"@effect/platform-bun": patch +"@effect/platform-deno": patch +"@effect/platform-node": patch +"effect": patch +--- + +Use SchemaBinary as the default RPC serialization for TCP cluster connections, including configurable frame limits. + +Cluster payloads are encoded with the binary codec on the wire. When a persisted reply cannot be encoded for JSON storage, the defect fallback that storage records is now also the reply delivered to waiting callers, so live replies always match what was persisted. + +SchemaBinary codecs are memoized by schema identity and wire mode, so per-message codec requests reuse the derived codec instead of rebuilding it. diff --git a/repos/effect/.changeset/template-literal-parts-without-encoding.md b/repos/effect/.changeset/template-literal-parts-without-encoding.md new file mode 100644 index 0000000000..246f3b36a2 --- /dev/null +++ b/repos/effect/.changeset/template-literal-parts-without-encoding.md @@ -0,0 +1,15 @@ +--- +"effect": patch +--- + +Separate template literal validation from transformed tuple parsing. `TemplateLiteralParser` now propagates its parts' decoding and encoding service requirements. + +### Breaking changes + +`Schema.TemplateLiteral` and `SchemaAST.TemplateLiteral` now throw during construction when a part contains an encoding, including inside unions and nested templates. This also rejects transformations whose decoded and encoded types are equal. Brands and supported checks without encodings remain valid. + +Use `Schema.Literals([0, 1])` to describe bit spellings or `Schema.Finite` to describe finite numeric spellings. Use `Schema.TemplateLiteralParser` when you need to decode transformed parts into a tuple. Explicit `Schema.toType` or `Schema.toEncoded` projections can remove an encoding, but do not necessarily preserve the strings accepted by the old template. For example, a `Finite` part rejects the empty segment accepted by `FiniteFromString`. + +`Schema.toEncoded(Schema.TemplateLiteralParser(...))` now validates the structure of the template instead of accepting any string. Use `Schema.String` when unrestricted strings are intended. + +When parser parts require services, provide those services to the corresponding decoding or encoding effect. These requirements were previously omitted from the parser's types. diff --git a/repos/effect/.changeset/testschema-own-field-asts.md b/repos/effect/.changeset/testschema-own-field-asts.md new file mode 100644 index 0000000000..42b34e7520 --- /dev/null +++ b/repos/effect/.changeset/testschema-own-field-asts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `TestSchema.Asserts.ast.fields.equals` to compare ASTs for all own struct fields, including symbol and non-enumerable keys. Equivalent field schemas now compare equally regardless of schema instance identity, while differing ASTs and distinct symbol keys remain unequal. diff --git a/repos/effect/.changeset/thin-ends-hug.md b/repos/effect/.changeset/thin-ends-hug.md new file mode 100644 index 0000000000..e3d613b8a2 --- /dev/null +++ b/repos/effect/.changeset/thin-ends-hug.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +dispatch websocket events directly diff --git a/repos/effect/.changeset/tidy-parts-narrow.md b/repos/effect/.changeset/tidy-parts-narrow.md new file mode 100644 index 0000000000..7e0cb7c7e9 --- /dev/null +++ b/repos/effect/.changeset/tidy-parts-narrow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix response tool part assignability after narrowing generic intersected tool records. diff --git a/repos/effect/.changeset/tidy-tools-finish.md b/repos/effect/.changeset/tidy-tools-finish.md new file mode 100644 index 0000000000..3a66ee3538 --- /dev/null +++ b/repos/effect/.changeset/tidy-tools-finish.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make automatic tool resolution interruption-safe for incomplete language model responses. diff --git a/repos/effect/.changeset/timeout-error-message.md b/repos/effect/.changeset/timeout-error-message.md new file mode 100644 index 0000000000..acfb35d2b1 --- /dev/null +++ b/repos/effect/.changeset/timeout-error-message.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Restore the `Effect.timeout` error message so `TimeoutError` includes the elapsed duration. diff --git a/repos/effect/.changeset/timeout-or-else-cleanup-before-fallback.md b/repos/effect/.changeset/timeout-or-else-cleanup-before-fallback.md new file mode 100644 index 0000000000..02eb8f7206 --- /dev/null +++ b/repos/effect/.changeset/timeout-or-else-cleanup-before-fallback.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `Effect.timeoutOrElse` to finish interrupting the source before evaluating the fallback, preventing the source from winning after the timeout. + +Fallbacks now run in the caller fiber and inherit its interruptibility and supervision. diff --git a/repos/effect/.changeset/token-bucket-elapsed-refill.md b/repos/effect/.changeset/token-bucket-elapsed-refill.md new file mode 100644 index 0000000000..6ee54efc45 --- /dev/null +++ b/repos/effect/.changeset/token-bucket-elapsed-refill.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix token-bucket `retryAfter`, `delay` and `resetAfter` in the memory and Redis stores. Timing now follows whole-token refill boundaries and accounts for elapsed time, including fractional token costs. Redis preserves signed fractional counts and keeps keys until capacity actually refills. + +`RateLimiterStore.tokenBucket` now returns `[remaining, elapsedMillis]` instead of `remaining`. Custom stores must return both values from the same atomic operation; see the `tokenBucket` docs for the contract. Returning `[remaining, 0]` keeps the old timing bug. diff --git a/repos/effect/.changeset/tokenizer-whole-prompt-truncation.md b/repos/effect/.changeset/tokenizer-whole-prompt-truncation.md new file mode 100644 index 0000000000..6258e5a228 --- /dev/null +++ b/repos/effect/.changeset/tokenizer-whole-prompt-truncation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Tokenizer.truncate` to account for token costs between messages. diff --git a/repos/effect/.changeset/tool-result-branch-encoding.md b/repos/effect/.changeset/tool-result-branch-encoding.md new file mode 100644 index 0000000000..5c609b07a7 --- /dev/null +++ b/repos/effect/.changeset/tool-result-branch-encoding.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Encode tool results with the schema for their known success or failure branch. diff --git a/repos/effect/.changeset/track-mapped-error-domain.md b/repos/effect/.changeset/track-mapped-error-domain.md new file mode 100644 index 0000000000..3c4c9a2c39 --- /dev/null +++ b/repos/effect/.changeset/track-mapped-error-domain.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.track(metric, mapper)` to reject source errors the mapper cannot handle. diff --git a/repos/effect/.changeset/trie-remove-valued-prefix.md b/repos/effect/.changeset/trie-remove-valued-prefix.md new file mode 100644 index 0000000000..beabebf6c5 --- /dev/null +++ b/repos/effect/.changeset/trie-remove-valued-prefix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve valued prefix nodes when removing a longer key from a `Trie`. diff --git a/repos/effect/.changeset/try-direct-error-types.md b/repos/effect/.changeset/try-direct-error-types.md new file mode 100644 index 0000000000..659c45466a --- /dev/null +++ b/repos/effect/.changeset/try-direct-error-types.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Correct the error types of `Effect.try` and `Effect.tryPromise`. Direct function forms retain `Cause.UnknownError`, while `{ try, catch }` options use the error type returned by `catch`. + +Explicit two-generic direct calls, union-valued arguments, and generic aliases that combine the two forms no longer compile. Use `{ try, catch }` with a real error mapper, or narrow a union before calling the constructor. + +Runtime behavior, callback arguments, and error mapping are unchanged. diff --git a/repos/effect/.changeset/tuple-optional-evolve-result-types.md b/repos/effect/.changeset/tuple-optional-evolve-result-types.md new file mode 100644 index 0000000000..c770854fd5 --- /dev/null +++ b/repos/effect/.changeset/tuple-optional-evolve-result-types.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Correct `Tuple.evolve` result types when a transform may be `undefined`. The result now includes both the transformed and unchanged element types, matching the existing runtime behavior. Accepted inputs and runtime behavior are unchanged. + +Code relying on the previous, incorrect result type must handle both outcomes. For example, a number-to-string transform that may be absent now produces `number | string`, so callers assuming a number-only result must adjust. diff --git a/repos/effect/.changeset/undici-response-form-data.md b/repos/effect/.changeset/undici-response-form-data.md new file mode 100644 index 0000000000..21a307e9ee --- /dev/null +++ b/repos/effect/.changeset/undici-response-form-data.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Parse URL-encoded and multipart response bodies in the Undici HTTP client. diff --git a/repos/effect/.changeset/unstable-http-schemas.md b/repos/effect/.changeset/unstable-http-schemas.md new file mode 100644 index 0000000000..809b62c34a --- /dev/null +++ b/repos/effect/.changeset/unstable-http-schemas.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Move the Cookie, Cookies, Headers, and UrlParams schemas from `effect/unstable/http` to `effect/Schema`, including their record and JSON-field helper schemas. diff --git a/repos/effect/.changeset/upgraded-request-skips-http-response-write.md b/repos/effect/.changeset/upgraded-request-skips-http-response-write.md new file mode 100644 index 0000000000..1748e362a7 --- /dev/null +++ b/repos/effect/.changeset/upgraded-request-skips-http-response-write.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Prevent NodeHttpServer from writing the route's HTTP response onto a connection that was upgraded to a WebSocket connection because stricter clients will interpret those bytes as WebSocket frames, logging "Invalid frame header" and failing the connection with an untyped 1006 error instead of the actual close code that the server sent. diff --git a/repos/effect/.changeset/urlparams-null-input.md b/repos/effect/.changeset/urlparams-null-input.md new file mode 100644 index 0000000000..274b0d09ea --- /dev/null +++ b/repos/effect/.changeset/urlparams-null-input.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `UrlParams.fromInput` to stringify `null` values. diff --git a/repos/effect/.changeset/urlparams-setall-immutable.md b/repos/effect/.changeset/urlparams-setall-immutable.md new file mode 100644 index 0000000000..25422eb6f0 --- /dev/null +++ b/repos/effect/.changeset/urlparams-setall-immutable.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent `UrlParams.setAll` from mutating reusable overrides. diff --git a/repos/effect/.changeset/use-canonical-array-indices.md b/repos/effect/.changeset/use-canonical-array-indices.md new file mode 100644 index 0000000000..a3bda5d217 --- /dev/null +++ b/repos/effect/.changeset/use-canonical-array-indices.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Treat only unpadded decimal integers from `0` through `4294967294` as array indices in environment-backed configuration and bracket-path decoding. This preserves numeric-looking object keys and prevents out-of-range environment keys from producing impossible array lengths. Bracket paths that intend to address arrays must use `[1]` instead of `[01]`. diff --git a/repos/effect/.changeset/violet-pugs-tickle.md b/repos/effect/.changeset/violet-pugs-tickle.md new file mode 100644 index 0000000000..7da0c9ae14 --- /dev/null +++ b/repos/effect/.changeset/violet-pugs-tickle.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Cluster shard-lock recovery no longer stalls behind a wedged reserved SQL connection. + +While lock storage is unhealthy, the empty liveness probe (`refresh(address, [])`) now runs on the shared pool instead of the reserved lock connection, so a hung reserved connection cannot block recovery. Failed probes are also logged as warnings instead of being silently swallowed. diff --git a/repos/effect/.changeset/vitest-five-migration.md b/repos/effect/.changeset/vitest-five-migration.md new file mode 100644 index 0000000000..349baff340 --- /dev/null +++ b/repos/effect/.changeset/vitest-five-migration.md @@ -0,0 +1,16 @@ +--- +"@effect/vitest": patch +"@effect/doctest": patch +--- + +Require Vitest `>=5.0.0 <6.0.0` and Node.js `^22.12.0 || ^24.0.0 || >=26.0.0`. + +### Breaking changes + +- Replace `.sequential` and `{ sequential: true }` with `{ concurrent: false }`. +- Use `bench` from the test context and await `bench(name, fn).run()`. The top-level benchmark API is removed. +- Use `Assertion` or `Assertion, T>`. Define custom matchers through `vitest.Matchers`, not `@vitest/expect`. +- Import reporter types from `vitest/node` and environment/snapshot APIs from `vitest/runtime`. Set `outputFile` when consuming JSON reports. +- Await asynchronous assertions. Mock history now clears before each test. + +See the [Vitest migration guide](https://vitest.dev/guide/migration/) for removed types and other upstream changes. diff --git a/repos/effect/.changeset/vitest-layer-concurrency.md b/repos/effect/.changeset/vitest-layer-concurrency.md new file mode 100644 index 0000000000..2b1922a264 --- /dev/null +++ b/repos/effect/.changeset/vitest-layer-concurrency.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Add a `concurrent` option to named `layer` and `it.layer` suites. Omitted options and anonymous layers preserve inherited concurrency. diff --git a/repos/effect/.changeset/with-error-reporting-effect-result.md b/repos/effect/.changeset/with-error-reporting-effect-result.md new file mode 100644 index 0000000000..f359ab0767 --- /dev/null +++ b/repos/effect/.changeset/with-error-reporting-effect-result.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Fix `Effect.withErrorReporting` to return an `Effect` instead of preserving input +subtypes such as `Exit`, whose subtype-specific fields are not present on the wrapper. diff --git a/repos/effect/.changeset/worker-run-early-exit.md b/repos/effect/.changeset/worker-run-early-exit.md new file mode 100644 index 0000000000..a5d68a6069 --- /dev/null +++ b/repos/effect/.changeset/worker-run-early-exit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Worker.run` hanging uninterruptibly when a worker dies before the ready handshake. diff --git a/repos/effect/.changeset/xhr-arraybuffer-readers.md b/repos/effect/.changeset/xhr-arraybuffer-readers.md new file mode 100644 index 0000000000..d3ce6d29e7 --- /dev/null +++ b/repos/effect/.changeset/xhr-arraybuffer-readers.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Support text, JSON, and stream readers when the XHR response type is `"arraybuffer"`. diff --git a/repos/effect/.github/actions/setup/action.yaml b/repos/effect/.github/actions/setup/action.yaml index bed1893225..0f97b61a17 100644 --- a/repos/effect/.github/actions/setup/action.yaml +++ b/repos/effect/.github/actions/setup/action.yaml @@ -13,11 +13,19 @@ runs: steps: - name: Install pnpm uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + env: + npm_config_audit: "false" + npm_config_fund: "false" - name: Install node uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - cache: pnpm node-version: 26.4.0 + package-manager-cache: ${{ env.NSC_CONTAINER_REGISTRY == '' }} + - name: Configure pnpm cache + if: ${{ env.NSC_CONTAINER_REGISTRY != '' }} + uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 + with: + cache: pnpm - name: Install deno uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2 if: ${{ inputs.deno-version != '' }} diff --git a/repos/effect/.github/workflows/ai-codegen.yml b/repos/effect/.github/workflows/ai-codegen.yml index d46bec4e34..6a88b2827a 100644 --- a/repos/effect/.github/workflows/ai-codegen.yml +++ b/repos/effect/.github/workflows/ai-codegen.yml @@ -15,7 +15,7 @@ jobs: codegen: name: AI Codegen if: github.repository_owner == 'Effect-Ts' - runs-on: ubuntu-latest + runs-on: namespace-profile-linux-small permissions: contents: write pull-requests: write diff --git a/repos/effect/.github/workflows/check.yml b/repos/effect/.github/workflows/check.yml index bbb1c979d7..d364540762 100644 --- a/repos/effect/.github/workflows/check.yml +++ b/repos/effect/.github/workflows/check.yml @@ -13,8 +13,8 @@ concurrency: permissions: {} jobs: - lint: - name: Lint + static-checks: + name: Static runs-on: ubuntu-latest permissions: contents: read @@ -23,11 +23,23 @@ jobs: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup - - run: pnpm lint + - name: Lint + run: pnpm lint + - name: Check circular dependencies + run: pnpm circular + - name: Generate AI documentation + run: pnpm ai-docgen + - name: Verify AI documentation is up-to-date + run: | + if [ -n "$(git status --short)" ]; then + git status --short + echo "Run 'pnpm ai-docgen' and commit generated changes." + exit 1 + fi types: name: Types - runs-on: ubuntu-latest + runs-on: namespace-profile-linux-small permissions: contents: read timeout-minutes: 10 @@ -35,7 +47,10 @@ jobs: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup + with: + deno-version: v2.9.4 - run: pnpm check + - run: deno check . - run: pnpm test-types --target '>=5.9' build: @@ -54,23 +69,6 @@ jobs: sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json - run: pnpm build - types-deno: - name: Types on Deno - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 10 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - name: Install dependencies - uses: ./.github/actions/setup - with: - deno-version: v2.9.4 - - name: Set strip internals config - run: | - sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json - - run: deno check . - bundle: name: Bundle if: github.event_name == 'pull_request' @@ -93,9 +91,10 @@ jobs: sed -i 's/"stripInternal": false/"stripInternal": true/' base/tsconfig.base.json - name: Build run: | - pnpm build & - cd base && pnpm install && pnpm build & - wait + pnpm build + cd base + pnpm install + pnpm build - name: Compare bundle size run: node ./packages/tools/bundle/src/bin.ts compare --base-dir base/packages/tools/bundle/fixtures - name: Upload stats artifact @@ -107,24 +106,36 @@ jobs: if-no-files-found: error test: - name: Test - runs-on: ubuntu-latest + name: Test (${{ matrix.runtime }}) + runs-on: namespace-profile-linux-default env: EFFECT_INTEGRATION_TESTS: "1" + # The ephemeral runner provides cleanup without a Ryuk sidecar. + TESTCONTAINERS_RYUK_DISABLED: "true" permissions: contents: read - timeout-minutes: 10 + timeout-minutes: 15 strategy: fail-fast: false matrix: - shard: [1/2, 2/2] - runtime: [Node, Deno] + include: + - runtime: Node + deno: "" + bun: "" + command: pnpm test --max-concurrency=10 + - runtime: Deno + deno: v2.9.4 + bun: "" + command: deno task test --max-concurrency=10 + - runtime: Bun + deno: "" + bun: 1.4.0 + command: bun run --bun vitest run --max-concurrency=10 steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Pre-pull test container images run: | - docker pull testcontainers/ryuk:0.14.0 & docker pull ghcr.io/tursodatabase/libsql-server:main & docker pull postgres:alpine & docker pull mysql:lts & @@ -133,40 +144,17 @@ jobs: docker pull redis:alpine & wait - - name: Install dependencies - if: matrix.runtime == 'Node' - uses: ./.github/actions/setup - - name: Test - if: matrix.runtime == 'Node' - run: pnpm test --shard ${{ matrix.shard }} - - - name: Install dependencies - if: matrix.runtime == 'Deno' - uses: ./.github/actions/setup - with: - deno-version: v2.9.4 - - name: Test - if: matrix.runtime == 'Deno' - run: deno task test --shard ${{ matrix.shard }} - - test-bun: - name: Test on Bun - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - name: Install dependencies uses: ./.github/actions/setup with: - bun-version: 1.3.13 + deno-version: ${{ matrix.deno }} + bun-version: ${{ matrix.bun }} - name: Test - run: bun node_modules/vitest/vitest.mjs run --project @effect/platform-bun + run: ${{ matrix.command }} doctest: - name: Documentation Tests - runs-on: ubuntu-latest + name: Test (Documentation) + runs-on: namespace-profile-linux-small permissions: contents: read timeout-minutes: 10 @@ -176,36 +164,3 @@ jobs: uses: ./.github/actions/setup - name: Test Documentation run: pnpm doctest - - ai-docgen: - name: AI Documentation Generation - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 10 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - name: Install dependencies - uses: ./.github/actions/setup - - name: Generate AI Documentation - run: pnpm ai-docgen - - name: Verify AI Documentation is up-to-date - run: | - if [ -n "$(git status --short)" ]; then - git status --short - echo "Run 'pnpm ai-docgen' and commit generated changes." - exit 1 - fi - - circular: - name: Circular Dependencies - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 10 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - name: Install dependencies - uses: ./.github/actions/setup - - name: Check for circular dependencies - run: pnpm circular diff --git a/repos/effect/.github/workflows/cluster.yml b/repos/effect/.github/workflows/cluster.yml index 606ee80189..ba674aed44 100644 --- a/repos/effect/.github/workflows/cluster.yml +++ b/repos/effect/.github/workflows/cluster.yml @@ -7,22 +7,17 @@ permissions: {} jobs: test: name: Test - runs-on: ubuntu-latest + runs-on: namespace-profile-linux-small timeout-minutes: 30 env: EFFECT_CLUSTER_TESTS: "1" + # The ephemeral runner provides cleanup without a Ryuk sidecar. + TESTCONTAINERS_RYUK_DISABLED: "true" permissions: contents: read steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - name: Pre-pull test container images - run: | - docker pull testcontainers/ryuk:0.14.0 & - docker pull postgres:alpine & - docker pull mysql:lts & - wait - - name: Install dependencies uses: ./.github/actions/setup - name: Test diff --git a/repos/effect/.github/workflows/release-queue.yml b/repos/effect/.github/workflows/release-queue.yml index cbf7d24721..1235f5adf5 100644 --- a/repos/effect/.github/workflows/release-queue.yml +++ b/repos/effect/.github/workflows/release-queue.yml @@ -27,7 +27,7 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 token: ${{ secrets.EFFECT_BOT_GH }} diff --git a/repos/effect/.github/workflows/snapshot.yml b/repos/effect/.github/workflows/snapshot.yml index 30fca3d8a3..7ce71e4a8f 100644 --- a/repos/effect/.github/workflows/snapshot.yml +++ b/repos/effect/.github/workflows/snapshot.yml @@ -15,7 +15,7 @@ permissions: {} jobs: approval-gate: if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository - runs-on: ubuntu-latest + runs-on: namespace-profile-linux-small environment: fork steps: - run: echo "Fork PR approved by maintainer." @@ -27,7 +27,7 @@ jobs: !cancelled() && (needs.approval-gate.result == 'success' || needs.approval-gate.result == 'skipped') && github.repository_owner == 'Effect-Ts' - runs-on: ubuntu-latest + runs-on: namespace-profile-linux-small timeout-minutes: 10 steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 diff --git a/repos/effect/.gitignore b/repos/effect/.gitignore index b9c2b253e4..26bba5ae55 100644 --- a/repos/effect/.gitignore +++ b/repos/effect/.gitignore @@ -1,24 +1,34 @@ # Generated by Direnv -.direnv/ .env +# Dot files and directories +**/.* +!/.agents/ +!/.changeset/ +!/.envrc +!/.github/ +!/.gitignore +!/.oxlintrc.json +!/.vscode/ + # Generated by TypeScript dist/ build/ +lib/ **/*.tsbuildinfo # Generated by Pnpm node_modules/ -.pnpm-store/ -# Auto-generated from scripts +# Generated by scripts coverage/ tmp/ +stats.txt # Generated by MacOS .DS_Store -# scratchpad files +# Scratchpad files scratchpad/**/* # Agent instructions @@ -27,22 +37,5 @@ scratchpad/**/* /packages/**/CLAUDE.md /packages/**/ai-docs/ -# lalph -.lalph/ -.repos/ - -# ralph auto loop (runtime output) -.ralph-auto/ - -# Claude Code -.claude/ - -# OpenCode local tooling -.opencode/ +# Agent config opencode.json - -# Repositories -.repos/ - -# oxlint data -.data/ diff --git a/repos/effect/.oxlintrc.json b/repos/effect/.oxlintrc.json index b27dad5455..b75ec470da 100644 --- a/repos/effect/.oxlintrc.json +++ b/repos/effect/.oxlintrc.json @@ -6,15 +6,23 @@ "**/dist", "**/build", "**/docs", - "**/.tsbuildinfo", + "**/*.tsbuildinfo", + "**/CHANGELOG.md", "packages/effect/typeperf/**/*", "packages/effect/src/StandardSchema.ts", - "packages/**/CHANGELOG.md", "!scratchpad/**/*", - ".changeset/**/*", - ".agents/**/*", - ".context/**/*", - ".specs/**/*" + "**/.*", + "!.agents", + "!.agents/**/*", + "!.changeset", + "!.changeset/**/*", + "!.envrc", + "!.github", + "!.github/**/*", + "!.gitignore", + "!.oxlintrc.json", + "!.vscode", + "!.vscode/**/*" ], "jsPlugins": ["@effect/oxc/oxlint"], "overrides": [{ diff --git a/repos/effect/.patterns/dynamic-records.md b/repos/effect/.patterns/dynamic-records.md deleted file mode 100644 index de4b25bb1d..0000000000 --- a/repos/effect/.patterns/dynamic-records.md +++ /dev/null @@ -1,41 +0,0 @@ -# Dynamic Record Safety - -An **open key** comes from external data or is determined at runtime. A key is -**closed** only when selected from an explicit internal list; TypeScript types -do not close external input at runtime. - -## Rules - -1. **Owned internal dictionary:** use - `Object.create(null) as Record` (or `Map` without record/JSON - interop). Direct open-key reads and writes are safe. Use `Object.hasOwn` when - stored `undefined` must differ from absence. -2. **External record:** use `Object.hasOwn(record, key)` for presence. Never use - `in`, truthiness, or `record[key] !== undefined`. Enumerate with - `Object.keys` / `values` / `entries`, never `for...in`. -3. **Normal object or instance:** write open keys with - `Record.assignProperty(target, key, value)`. It protects only writes; - presence checks still require `Object.hasOwn`. -4. **Existing public API:** preserve its output prototype. Build with a - null-prototype dictionary, then return `{ ...internalMap }` when a normal - object is required. - -## `Object.assign` - -| Case | Policy | -| ------------------------------------ | ----------------------------------------------------- | -| Null-rooted target + open source | `Object.assign` allowed | -| New normal target + open source | Use `{ ...source }` | -| New custom-prototype object | Use `Object.setPrototypeOf({ ...source }, Proto)` | -| Existing normal target + open source | Copy own enumerable keys with `Record.assignProperty` | -| Normal target + closed source | `Object.assign` allowed | - -Therefore `Object.assign({}, openSource)` is forbidden. A source is closed only -when constructed internally with explicit properties, for example -`{ name: options.name }`; passing `options` directly is not closed. -Use `Reflect.ownKeys` plus an enumerable check when symbols must be copied. - -## Property Syntax - -- Safe: `{ [key]: value }` and `{ ...source }`; both create own data properties. -- Unsafe for data: `{ __proto__: value }`; it changes the literal's prototype. diff --git a/repos/effect/.patterns/effect.md b/repos/effect/.patterns/effect.md deleted file mode 100644 index a778ae85c6..0000000000 --- a/repos/effect/.patterns/effect.md +++ /dev/null @@ -1,102 +0,0 @@ -# Effect Library Development Patterns - -## NEVER: try-catch in Effect.gen - -**REASON**: Effect generators handle errors through the Effect type system, not JavaScript exceptions. - -```typescript -// ❌ WRONG - This will cause runtime errors -Effect.gen(function*() { - try { - const result = yield* someEffect - return result - } catch (error) { - // This will never be reached and breaks Effect semantics - console.error(error) - } -}) - -// ✅ CORRECT - Use Effect's built-in error handling -Effect.gen(function*() { - const result = yield* Effect.result(someEffect) - if (result._tag === "Failure") { - // Handle error case properly - console.error("Effect failed:", result.cause) - return yield* Effect.fail("Handled error") - } - return result.value -}) -``` - -## return yield* Pattern for Errors - -**CRITICAL**: Always use `return yield*` when yielding terminal effects. - -```typescript -// ✅ CORRECT - Makes termination explicit -Effect.gen(function*() { - if (invalidCondition) { - return yield* Effect.fail("Validation failed") - } - - if (shouldInterrupt) { - return yield* Effect.interrupt - } - - // Continue with normal flow - const result = yield* someOtherEffect - return result -}) - -// ❌ WRONG - Missing return keyword leads to unreachable code -Effect.gen(function*() { - if (invalidCondition) { - yield* Effect.fail("Validation failed") // Missing return! - // Unreachable code after error! - } -}) -``` - -## `Effect.gen` and `Effect.fnUntraced` - -Prefer `Effect.fnUntraced` over functions that only return `Effect.gen`. - -```typescript -// ❌ AVOID - Function only wraps Effect.gen -const fn = (param: string) => - Effect.gen(function*() { - // ... - }) - -// ✅ PREFER - Reusable untraced Effect function -const fn = Effect.fnUntraced(function*(param: string) { - // ... -}) -``` - -## When to Use What - -**Use `Effect.gen`** when: - -- Writing inline effect composition -- One-off operations that don't need to be reused -- Inside other functions already being traced - -**Use `Effect.fnUntraced`** when: - -- Building library implementations -- Performance is critical (hot paths) -- Function is called many times per operation -- Tracing overhead is unacceptable - -## `Context.Service` - -Prefer the class syntax when working with `Context.Service`. - -```typescript -import { Context } from "effect" - -class MyService extends Context.Service number -}>()("MyService") {} -``` diff --git a/repos/effect/.patterns/jsdoc.md b/repos/effect/.patterns/jsdoc.md deleted file mode 100644 index a3ac911f35..0000000000 --- a/repos/effect/.patterns/jsdoc.md +++ /dev/null @@ -1,136 +0,0 @@ -# JSDoc Patterns - -## `@category` Guidance - -When adding or vetting JSDoc categories in public source files: - -- Use exactly one `@category` tag for each public JSDoc block that represents a documented API. -- Use shared categories consistently across the repository. Domain-specific categories are allowed when they improve navigation within a file or package, but avoid one-off categories unless they name an important API/domain concept. -- Prefer lowercase category names by default, plural nouns for API buckets, and gerunds for operation families. -- Preserve canonical casing for acronyms and proper API/domain names, such as `type IDs`, `DateTime`, `Undici`, and `HttpAgent`. -- Prefer shared API-shape categories for common Effect/library patterns, and use domain-topic categories only when they provide clearer navigation. -- Avoid vague fallback categories. Do not use `utils`, `common`, or `misc`; pick a specific shared or domain category instead. - -## Common Shared Categories - -- API shapes: `constructors`, `destructors`, `models`, `schemas`, `guards`, `predicates`, `getters`, `accessors`, `instances`, `constants`, `protocols`, `prototypes`, `re-exports`, `unsafe`, `testing` -- Effect/service concepts: `services`, `tags`, `layers`, `context`, `resource management`, `running` -- Type-level APIs: `utility types` for type-level helpers/contracts; use `models` for exported type/interface/class shapes that represent domain data -- Error APIs: `errors` for error models/classes/types, `error handling` for recovery/catching/mapping APIs -- Operations: `combinators`, `filtering`, `mapping`, `sequencing`, `zipping`, `combining`, `merging`, `converting`, `transforming`, `folding`, `splitting`, `repetition` -- Encoding/data formats: `encoding`, `decoding`, `serialization` -- Observability: `tracing`, `metrics`, `logging` -- Other common concepts: `annotations`, `references`, `symbols`, `type IDs`, `configuration`, `math`, `comparisons`, `ordering` - -## Category Normalization - -Normalize category names before adding or reviewing JSDoc: - -- Lowercase plain category names. Preserve established acronyms and proper - names, such as `type IDs`, `DateTime`, `JSON getters`, `Base64 getters`, and - `Standard Schema`. -- Prefer shared plural buckets when the meaning is the same, such as - `constructors`, `models`, `schemas`, `guards`, `getters`, `services`, - `layers`, `generators`, `subscriptions`, `cookies`, and `sizes`. -- Prefer shared operation families over narrow synonyms when precision is not - important, such as `combining`, `mapping`, `filtering`, `folding`, - `converting`, `transforming`, `sequencing`, and `repetition`. -- Replace vague fallback categories such as `utils`, `common`, `misc`, or - `helpers` with a specific shared or domain category. -- Use `services` for `Context.Service` and `Context.Reference` exports, and - use `tags` only for `Context.Tag` exports. -- Fix obvious typos and compact variants during cleanup, such as - `transferables`, `re-exports`, `resource management`, and `Standard Schema`. - -## Distinctions - -Keep these distinctions: - -- `services` are `Context.Service` / `Context.Reference` exports and service contracts/shapes, `tags` are `Context.Tag` exports, and `layers` provide services. -- `getters` retrieve values/properties, while `accessors` are contextual service or environment access helpers. -- `errors` are error data types, while `error handling` is for APIs that handle failures. -- `models` describe domain/API data structures, while `schemas` are schema values/combinators and `utility types` are type-level helpers/contracts. -- `guards` are TypeScript type guards, `predicates` are boolean tests, and `filtering` is for filtering operations. - -## Example Best Practices - -### Quality Checklist - -Use this checklist when authoring or reviewing an example: - -- **Classify execution:** Make the example clearly one of a runnable observation, typechecked definition, test registration, - runtime entrypoint, or external-infrastructure illustration. Do not combine alternative runtimes or deployment paths in - one executable module; present them as separately labeled, non-evaluated alternatives. -- **Order setup, operation, observation:** Make the documented API and its result scannable. Inline simple setup into the - assertion; otherwise arrange setup first, the operation second, and a separate observation block after one blank line. -- **Teach one primary semantic contract:** Include only the adjacent concepts needed to observe that behavior. Remove unused - errors, services, imports, alternate programs, and fictional generic-type scaffolding. Integration examples may include - more concepts only when the integration is the lesson. -- **Observe the promised semantic boundary:** Assert the full semantic value when practical. If unstable or irrelevant data - requires a projection, choose stable fields that distinguish the promised behavior from neighboring outcomes. For - example, prefer `Exit.fail("missing")` over observing only `_tag === "Failure"`. -- **Prefer direct observation:** Use the abstraction's return value, collector, or fold before introducing a mutable probe. - Console output or successful execution alone does not establish semantic behavior. - - Good: - - ```ts - await Effect.runPromise(Stream.runCollect(Stream.make(1, 2, 3))) // => [1, 2, 3] - ``` - - Counterexample: - - ```ts - const values: Array = [] - - await Effect.runPromise( - Stream.make(1, 2, 3).pipe(Stream.runForEach((value) => Effect.sync(() => values.push(value)))) - ) - values // => [1, 2, 3] - ``` - -- **Use local probes only when the API has no direct result:** A local mutable probe is appropriate for - `acquireRelease`/finalizers and callback-oriented APIs when lifecycle order or emitted events are the contract. Keep the - probe local and sequential. - - ```ts - const events: Array = [] - const resource = Effect.acquireRelease( - Effect.sync(() => events.push("acquire")), - () => Effect.sync(() => events.push("release")) - ) - - await Effect.runPromise(Effect.scoped(resource)) - events // => ["acquire", "release"] - ``` - -- **Use Effect-managed observers for concurrency:** Prefer `Ref`, `Deferred`, or `Queue` over mutable arrays or flags when - fibers, concurrent consumers, interruption, or races are part of the behavior. -- **Keep execution bounded and deterministic:** Bound retries, repeats, polling, generated streams, tool loops, and - concurrent consumers unless non-termination is the documented entrypoint behavior. Avoid live clocks, randomness, - scheduling accidents, external services, machine-specific state, and unawaited work; use controlled inputs and ensure - cleanup completes. -- **Choose runners deliberately:** Prefer awaited `Effect.runPromise` in runnable examples. Use `Effect.runSync` only when - synchronous execution is the documented contract or materially clarifies an Effect known to be synchronous; do not use - it merely as a shorter doctest runner. -- **Progress multiple examples by behavior:** Move from basic success to a defining boundary or failure, then composition or - lifecycle behavior. Do not repeat equivalent happy paths with renamed values or alternate syntax unless the calling style - or overload dispatch is itself part of the contract. -- **Keep type-only examples type-only:** Retain runnable metadata for extraction and typechecking, but do not add tautological - runtime assertions such as assigning a typed literal and asserting that the literal is unchanged. Add an assertion only - when the API also performs runtime transformation or validation. - -### Doctest Mechanics - -- Mark runnable TypeScript examples with `````ts import.meta.vitest`` so `pnpm doctest` executes them. -- Use a trailing `// =>` comment to assert an expression or single initialized `const` identifier against a TypeScript expression on the same line. Values use Effect's `Equal.equals` semantics, and examples without markers remain execution-only. Write asynchronous execution explicitly; the transform does not run Effects or await promises automatically. -- Prefer asserting the API call directly. Keep bindings only for reuse, mutation, identity checks, or meaningful multi-step setup; put a blank line before a separate assertion block. -- Keep calls on one line when the complete line is at most 120 characters. Format expected arrays densely (`[1, 2]`, `[[1], [2]]`, `Option.some([1, 2])`) while retaining normal object spacing. -- Assert semantic constructors such as `Option.some`, `Result.succeed`, and `Exit.fail`, not rendered console output. Preserve runnable markers on type-level examples without adding fake runtime assertions. -- Keep runnable examples complete, deterministic, bounded, and independent of external services or machine-specific state. Await asynchronous work so failures and cleanup remain inside the doctest. -- Import public APIs and include all required setup. Do not use undeclared placeholders or rely on declarations from surrounding prose. -- Leave examples that register Vitest tests or suites as plain `````ts`` fences; the doctest collector executes runnable - snippets inside tests, where nested test registration is invalid. Invoke registration APIs directly so the snippet still - shows the intended top-level usage. -- Leave intentionally non-executable snippets as plain `````ts`` fences. -- Run `pnpm doctest --run ` from the repository root after changing runnable examples. diff --git a/repos/effect/.patterns/testing.md b/repos/effect/.patterns/testing.md deleted file mode 100644 index 119f4f17ea..0000000000 --- a/repos/effect/.patterns/testing.md +++ /dev/null @@ -1,76 +0,0 @@ -# Testing Patterns - -## Testing Framework Selection - -Use `it.effect` for tests that return Effects. - -`it.effect` and `it.live` each provide and close a `Scope` for every test. Return scoped effects directly; do not wrap -the test body in `Effect.scoped`. - -```typescript -import { assert, describe, it } from "@effect/vitest" -import { Effect } from "effect" - -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 unit tests. Runnable documentation may use it only for the intentional synchronous-runner - cases described in `.patterns/jsdoc.md`. -- Never use `expect` from Vitest; use `assert` methods instead -- Always use `TestClock` for time-dependent operations -- Group related tests using `describe` - -## Type-Level Tests - -Type-level tests are located in `packages/*/typetest/` and use Tstyche. - -Run targeted type-level tests with: - -```sh -pnpm test-types -``` - -### Testing Displayed Types - -Ordinary Tstyche assertions such as `toBe` compare types structurally. They cannot -catch regressions where a public type is semantically correct but TypeScript -displays an internal alias or an unsimplified intersection in editor quick info. - -To test the displayed form, deliberately produce an assignment error and use -Tstyche's checked `@ts-expect-error` message to match a distinctive substring of -the rendered type: - -```typescript -it("simplifies the displayed type", () => { - const value = null as unknown as PublicType - - // @ts-expect-error Type '{ readonly value: string; }' - const displayed: never = value - - void displayed -}) -``` - -Before accepting the test, temporarily restore the broken type and confirm that -the diagnostic-message match fails. Keep the expected substring as small as -possible while still distinguishing the desired public type from the leaked -implementation type, because diagnostic wording can change between TypeScript -versions. Run the targeted test against every TypeScript version configured by -`pnpm test-types`. diff --git a/repos/effect/.specs/README.md b/repos/effect/.specs/README.md deleted file mode 100644 index c6abd9919e..0000000000 --- a/repos/effect/.specs/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Specifications - diff --git a/repos/effect/LLMS.md b/repos/effect/LLMS.md index b3021967ef..7e2ee6e8a5 100644 --- a/repos/effect/LLMS.md +++ b/repos/effect/LLMS.md @@ -11,9 +11,11 @@ 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. +Prefer `Effect.gen` for inline Effect code. For reusable functions, prefer +`Effect.fn("name")` when tracing is useful and `Effect.fnUntraced` when it is not, +particularly in library implementations and hot paths. Avoid functions that only +wrap and return `Effect.gen`. Attach additional behaviour with combinators; this +style is more readable and easier to maintain than using combinators alone. ### Using Effect.gen @@ -46,13 +48,16 @@ export class FileProcessingError extends Schema.TaggedError }) {} ``` -### Using Effect.fn +### Using Effect.fn and Effect.fnUntraced -When writing functions that return an Effect, use `Effect.fn` to use the -generator syntax. +When writing reusable functions that return an Effect, use `Effect.fn` or +`Effect.fnUntraced` to use the generator syntax. -**Avoid creating functions that return an Effect.gen**, use `Effect.fn` -instead. +Use `Effect.fn("name")` when the function should create a tracing span. Prefer +`Effect.fnUntraced` when tracing is not needed, particularly for library +implementations and hot paths. + +**Avoid creating functions that only wrap and return an `Effect.gen`**. ```ts import { Effect, Schema } from "effect" @@ -80,6 +85,16 @@ export const effectFunction = Effect.fn("effectFunction")( }) ) +// Effect.fnUntraced avoids tracing and stack-frame capture while still reusing +// the generator body. This is preferred for library functions that do not +// represent a useful tracing boundary. +export const validateBatchSize = Effect.fnUntraced(function*(size: number): Effect.fn.Return { + if (!Number.isInteger(size) || size <= 0) { + return yield* new SomeError({ message: "Batch size must be a positive integer" }) + } + return size +}) + // Use Schema.TaggedError to define a custom error export class SomeError extends Schema.TaggedError()("SomeError", { message: Schema.String @@ -247,7 +262,7 @@ They let you model finite or infinite data sources. - `NodeStream.fromReadable` for Node.js readable streams - **[Consuming and transforming streams](./ai-docs/src/03_stream/20_consuming-streams.ts)**: How to transform and consume streams using operators like `map`, `flatMap`, `filter`, `mapEffect`, and various `run*` methods. - **[Decoding and encoding streams](./ai-docs/src/03_stream/30_encoding.ts)**: - Use `Stream.pipeThroughChannel` with the `Ndjson` & `Msgpack` modules to + Use `Stream.pipeThroughChannel` with the `Ndjson` and `SchemaBinary` modules to decode and encode streams of structured data. ## Integrating Effect into existing applications diff --git a/repos/effect/ai-docs/package.json b/repos/effect/ai-docs/package.json index 90a59f266f..be4bf47a57 100644 --- a/repos/effect/ai-docs/package.json +++ b/repos/effect/ai-docs/package.json @@ -27,8 +27,8 @@ "@effect/sql-sqlite-wasm": "workspace:*", "@effect/vitest": "workspace:*", "effect": "workspace:*", - "hono": "^4.13.3", - "nodemailer": "^9.0.5" + "hono": "^4.13.7", + "nodemailer": "^10.0.0" }, "devDependencies": { "@types/nodemailer": "^8.0.1" diff --git a/repos/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts b/repos/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts index 1d41aaa154..14ba38de72 100644 --- a/repos/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +++ b/repos/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts @@ -1,11 +1,14 @@ /** - * @title Using Effect.fn + * @title Using Effect.fn and Effect.fnUntraced * - * When writing functions that return an Effect, use `Effect.fn` to use the - * generator syntax. + * When writing reusable functions that return an Effect, use `Effect.fn` or + * `Effect.fnUntraced` to use the generator syntax. * - * **Avoid creating functions that return an Effect.gen**, use `Effect.fn` - * instead. + * Use `Effect.fn("name")` when the function should create a tracing span. Prefer + * `Effect.fnUntraced` when tracing is not needed, particularly for library + * implementations and hot paths. + * + * **Avoid creating functions that only wrap and return an `Effect.gen`**. */ import { Effect, Schema } from "effect" @@ -33,6 +36,16 @@ export const effectFunction = Effect.fn("effectFunction")( }) ) +// Effect.fnUntraced avoids tracing and stack-frame capture while still reusing +// the generator body. This is preferred for library functions that do not +// represent a useful tracing boundary. +export const validateBatchSize = Effect.fnUntraced(function*(size: number): Effect.fn.Return { + if (!Number.isInteger(size) || size <= 0) { + return yield* new SomeError({ message: "Batch size must be a positive integer" }) + } + return size +}) + // Use Schema.TaggedError to define a custom error export class SomeError extends Schema.TaggedError()("SomeError", { message: Schema.String diff --git a/repos/effect/ai-docs/src/01_effect/01_basics/index.md b/repos/effect/ai-docs/src/01_effect/01_basics/index.md index ae13d7a31c..001846557d 100644 --- a/repos/effect/ai-docs/src/01_effect/01_basics/index.md +++ b/repos/effect/ai-docs/src/01_effect/01_basics/index.md @@ -1,5 +1,7 @@ ## 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. +Prefer `Effect.gen` for inline Effect code. For reusable functions, prefer +`Effect.fn("name")` when tracing is useful and `Effect.fnUntraced` when it is not, +particularly in library implementations and hot paths. Avoid functions that only +wrap and return `Effect.gen`. Attach additional behaviour with combinators; this +style is more readable and easier to maintain than using combinators alone. diff --git a/repos/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts b/repos/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts index 99bff184f9..fe73d0e2df 100644 --- a/repos/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts +++ b/repos/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts @@ -14,7 +14,7 @@ export const SqlClientLayer: Layer.Layer< PgClient.PgClient | SqlClient.SqlClient, Config.ConfigError | SqlError.SqlError > = PgClient.layerConfig({ - url: Config.redacted("DATABASE_URL") + url: Config.Redacted("DATABASE_URL") }) export class UserRespositoryError extends Schema.TaggedError()("UserRespositoryError", { diff --git a/repos/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts b/repos/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts index 75a8488eca..3f0883f285 100644 --- a/repos/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +++ b/repos/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts @@ -51,7 +51,7 @@ export class MessageStore extends Context.Service()( diff --git a/repos/effect/ai-docs/src/71_ai/30_chat.ts b/repos/effect/ai-docs/src/71_ai/30_chat.ts index f90ea1af65..745d4d0334 100644 --- a/repos/effect/ai-docs/src/71_ai/30_chat.ts +++ b/repos/effect/ai-docs/src/71_ai/30_chat.ts @@ -14,7 +14,7 @@ import { FetchHttpClient } from "effect/unstable/http" // --------------------------------------------------------------------------- const OpenAiClientLayer = OpenAiClient.layerConfig({ - apiKey: Config.redacted("OPENAI_API_KEY") + apiKey: Config.Redacted("OPENAI_API_KEY") }).pipe(Layer.provide(FetchHttpClient.layer)) // --------------------------------------------------------------------------- diff --git a/repos/effect/cookbooks/schedule.md b/repos/effect/cookbooks/schedule.md deleted file mode 100644 index 2ff56da206..0000000000 --- a/repos/effect/cookbooks/schedule.md +++ /dev/null @@ -1,563 +0,0 @@ -# Schedule Cookbook - -Use this cookbook when you need to define a `Schedule` value. The examples are -ordered from small single-purpose policies to larger real-world policies that -combine timing, input classification, output shaping, and observation. - -This cookbook intentionally defines schedules only. It does not apply them with -`Effect.retry`, `Effect.repeat`, streams, or channels. - -## Before Choosing A Schedule - -- `Schedule.recurs(n)` counts recurrences after the first run. -- `Schedule.spaced` waits after each completed run; `Schedule.fixed` uses an - aligned cadence; `Schedule.windowed` recurs on window boundaries. -- `Schedule.duration` performs exactly one recurrence after the duration. -- `Schedule.during` is an elapsed-time budget, not a delay by itself. -- Schedule output is policy output. Use `Schedule.passthrough` to preserve the - latest input, or `Schedule.map` to derive a new output from schedule metadata. -- `Schedule.max` continues only while all schedules continue and outputs the slowest delay. -- `Schedule.min` continues while any schedule can continue and outputs the fastest delay. -- `Schedule.jittered` spreads callers out. It does not add a recurrence limit. -- `Schedule.addDelay` adds extra delay based on schedule metadata. -- `Schedule.modifyDelay` replaces or adjusts the selected delay. -- Leave unbounded schedules to explicitly owned background work. - -## Choose By Problem Shape - -| Problem shape | Start with | -| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Bounded retry | `Schedule.exponential` or `Schedule.fibonacci`, then `Schedule.recurs` and optionally `Schedule.during` | -| Poll latest status | `Schedule.spaced` or `Schedule.fixed`, then `Schedule.setInputType`, `Schedule.passthrough`, and `Schedule.while` | -| Adapt delay from metadata | `Schedule.addDelay` | -| Replace or cap selected delay | `Schedule.modifyDelay` | -| Run phases in sequence | `Schedule.concat` | -| Preserve phase in output | `Schedule.concatResult` | -| Continue while all policies continue | `Schedule.max` | -| Continue while any policy continues | `Schedule.min` | -| Shape output from metadata | `Schedule.map` | -| Observe decisions without changing output | `Schedule.tap` | - -## Table Of Contents - -1. [Single-Policy Schedules](#single-policy-schedules) -2. [Shape Schedule Outputs](#shape-schedule-outputs) -3. [Combine Policies](#combine-policies) -4. [Work With Inputs](#work-with-inputs) -5. [Adapt Delays](#adapt-delays) -6. [Observe Schedule Decisions](#observe-schedule-decisions) -7. [Realistic Policies](#realistic-policies) - -## Single-Policy Schedules - -### Retry A Profile Fetch Three Times - -Goal: Create a retry policy for loading a user profile that allows at most 3 -recurrences and outputs the recurrence count. - -```ts -import { Schedule } from "effect" - -const profileRetry = Schedule.recurs(3) -``` - -### Poll A Queue Once Per Second - -Goal: Create a cadence for polling queue depth about 1 second after each -completed check. - -```ts -import { Schedule } from "effect" - -const queueDepthPolling = Schedule.spaced("1 second") -``` - -### Run A Heartbeat On An Aligned Cadence - -Goal: Create a heartbeat cadence that runs on aligned 30-second boundaries -instead of waiting 30 seconds after each run finishes. - -```ts -import { Schedule } from "effect" - -const heartbeatCadence = Schedule.fixed("30 seconds") -``` - -### Warm A Cache Once After Deployment - -Goal: Create a follow-up cache warmup that recurs exactly once after about 1 -minute. - -```ts -import { Schedule } from "effect" - -const cacheWarmupFollowUp = Schedule.duration("1 minute") -``` - -### Keep A Health Probe Inside A Time Budget - -Goal: Create a budget that allows health probes to continue only while about 1 -minute has elapsed or less. - -```ts -import { Schedule } from "effect" - -const healthProbeBudget = Schedule.during("1 minute") -``` - -### Retry A Config Fetch With Exponential Backoff - -Goal: Create a retry policy for fetching remote configuration, starting with a -100 millisecond exponential backoff. - -```ts -import { Schedule } from "effect" - -const configFetchBackoff = Schedule.exponential("100 millis") -``` - -### Probe A Cold Replica With Fibonacci Backoff - -Goal: Create a gentler startup probe for a cold search replica, using Fibonacci -backoff and taking the first 4 outputs. - -```ts -import { Schedule } from "effect" - -const searchReplicaWarmup = Schedule.fibonacci("100 millis").pipe( - Schedule.upTo({ times: 4 }) -) -``` - -### Run A Worker Loop Forever - -Goal: Create an unbounded worker-loop counter with no added delay. - -```ts -import { Schedule } from "effect" - -const workerLoopCounter = Schedule.forever -``` - -### Trigger A Nightly Report - -Goal: Create a schedule for a nightly billing report at 02:00. - -```ts -import { Schedule } from "effect" - -const nightlyBillingReport = Schedule.cron("0 2 * * *") -``` - -### Sample Five-Minute Windows - -Goal: Create a schedule that recurs on 5-minute window boundaries. - -```ts -import { Schedule } from "effect" - -const fiveMinuteWindows = Schedule.windowed("5 minutes") -``` - -## Shape Schedule Outputs - -### Echo Feature Flag Inputs - -Goal: Create a schedule for feature flag snapshots that immediately outputs -each input unchanged and takes the first 3 samples. - -```ts -import { Schedule } from "effect" - -type FeatureFlagSnapshot = { readonly enabled: boolean } - -const featureFlagSamples = Schedule.identity().pipe( - Schedule.upTo({ times: 3 }) -) -``` - -### Label Retry Attempts - -Goal: Create a retry-attempt schedule that turns recurrence count metadata into -labels such as `attempt-1`, `attempt-2`, and `attempt-3`. - -```ts -import { Schedule } from "effect" - -const retryAttemptLabels = Schedule.recurs(3).pipe( - Schedule.map(({ output: count }) => `attempt-${count + 1}`) -) -``` - -Explanation: `Schedule.map` receives the full step metadata. Destructure -`output` when you only need the schedule output, or use fields such as `input`, -`attempt`, `duration`, and `elapsed` when the new output needs more context. - -## Combine Policies - -### Add Jitter To Webhook Backoff - -Goal: Create a webhook retry backoff that starts at 200 milliseconds, adds -jitter, and takes 3 outputs. - -```ts -import { Schedule } from "effect" - -const jitteredWebhookBackoff = Schedule.exponential("200 millis").pipe( - Schedule.jittered, - Schedule.upTo({ times: 3 }) -) -``` - -### Stop Deployment Hook Retries By Count And Time - -Goal: Create a deployment hook retry budget that uses jittered exponential -backoff, allows at most 5 recurrences, and also stops after about 20 seconds. - -```ts -import { Schedule } from "effect" - -const deploymentHookRetryBudget = Schedule.max([ - Schedule.exponential("200 millis").pipe(Schedule.jittered), - Schedule.recurs(5), - Schedule.during("20 seconds") -]) -``` - -Explanation: `Schedule.max` stops when any schedule stops and outputs the -slowest selected delay for each recurrence. - -### Continue While Any Probe Is Active - -Goal: Create a service readiness policy that continues while either 2 immediate -warmup probes or a slower 500 millisecond probe schedule still wants to recur, -using the fastest selected delay. - -```ts -import { Schedule } from "effect" - -const readinessWarmupOrSlowProbe = Schedule.min([ - Schedule.recurs(2), - Schedule.spaced("500 millis").pipe(Schedule.upTo({ times: 5 })) -]) -``` - -Explanation: `Schedule.min` keeps recurring while at least one schedule can -continue and outputs the fastest selected delay among schedules that are still -recurring. - -### Warm Up Fast, Then Settle Into Maintenance - -Goal: Create a cache invalidation sequence that runs 2 quick recurrences 100 -milliseconds apart, then 3 slower recurrences 30 seconds apart, then stops. - -```ts -import { Schedule } from "effect" - -const cacheInvalidationSequence = Schedule.spaced("100 millis").pipe( - Schedule.upTo({ times: 2 }), - Schedule.concat(Schedule.spaced("30 seconds").pipe(Schedule.upTo({ times: 3 }))) -) -``` - -### Preserve Warmup And Steady Phases - -Goal: Create a retry classifier with a fast exponential phase and a steady -Fibonacci phase, preserving the phase in the output. - -```ts -import { Result, Schedule } from "effect" - -const phasedRetryClassifier = Schedule.exponential("100 millis").pipe( - Schedule.upTo({ times: 2 }), - Schedule.concatResult(Schedule.fibonacci("500 millis").pipe(Schedule.upTo({ times: 3 }))), - Schedule.map(({ output: result }) => - Result.match(result, { - onFailure: (delay) => ({ phase: "fast", delay }), - onSuccess: (delay) => ({ phase: "steady", delay }) - }) - ) -) -``` - -Explanation: `Schedule.concatResult` keeps phase information in the output. -The first schedule is represented by the failure side, and the second schedule -is represented by the success side. - -## Work With Inputs - -### Poll Upload Progress Until Complete - -Goal: Create an upload-progress schedule that waits about 1 second between -checks, outputs the latest progress object, and continues only while the upload -is incomplete. - -```ts -import { Schedule } from "effect" - -type UploadProgress = { readonly percent: number } - -const uploadProgressUntilComplete = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input.percent < 100) -) -``` - -Explanation: `Schedule.setInputType` tells TypeScript which input each step -receives. `Schedule.passthrough` makes the output the latest input, so a polling -schedule can return the final status instead of a counter. - -## Adapt Delays - -### Slow A Queue Consumer Under Backpressure - -Goal: Create a queue backpressure schedule that outputs each queue snapshot, -continues while the queue is not paused, adds 5 seconds of delay when depth is -above 1000, adds 500 milliseconds otherwise, and takes 10 outputs. - -```ts -import { Effect, Schedule } from "effect" - -type QueueSnapshot = { readonly depth: number; readonly paused: boolean } - -const queueBackpressureSchedule = Schedule.identity().pipe( - Schedule.while(({ input }) => !input.paused), - Schedule.addDelay(({ output: snapshot }) => Effect.succeed(snapshot.depth > 1000 ? "5 seconds" : "500 millis")), - Schedule.upTo({ times: 10 }) -) -``` - -Explanation: `Schedule.addDelay` receives the full step metadata and adds the -returned delay to the selected delay. It is a good fit for input-driven pacing -when the output is already the latest input. - -### Cap WebSocket Reconnect Delays - -Goal: Create a reconnect policy that uses jittered exponential backoff, caps -selected delays at 5 seconds, allows at most 8 recurrences, and outputs the -selected delay. - -```ts -import { Duration, Effect, Schedule } from "effect" - -const websocketReconnectDelays = Schedule.max([ - Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, Duration.seconds(5)))) - ), - Schedule.recurs(8) -]) -``` - -Explanation: `Schedule.modifyDelay` receives the full step metadata, including -the selected delay as `duration`, and returns the replacement delay. Use it for -caps, floors, clamps, or provider-provided delay hints. - -## Observe Schedule Decisions - -### Log Heartbeat Inputs - -Goal: Create a heartbeat schedule that runs on an aligned 10-second cadence, -logs each service id input, outputs the input unchanged, and takes 2 outputs. - -```ts -import { Console, Schedule } from "effect" - -type HeartbeatStatus = { readonly id: string } - -const heartbeatInputLogs = Schedule.fixed("10 seconds").pipe( - Schedule.setInputType(), - Schedule.tap(({ input }) => Console.log(`heartbeat:${input.id}`)), - Schedule.passthrough, - Schedule.upTo({ times: 2 }) -) -``` - -### Record Backoff Delays - -Goal: Create a retry schedule that uses Fibonacci backoff, takes 5 outputs, and -logs each selected delay without changing the schedule output. - -```ts -import { Console, Schedule } from "effect" - -const loggedBackoffDelays = Schedule.fibonacci("200 millis").pipe( - Schedule.upTo({ times: 5 }), - Schedule.tap(({ output: delay }) => Console.log(delay)) -) -``` - -### Log Attempt Metadata - -Goal: Create a telemetry backoff that logs each attempt number and selected -delay in milliseconds without changing the schedule output. - -```ts -import { Console, Duration, Schedule } from "effect" - -const telemetryBackoffPolicy = Schedule.exponential("250 millis").pipe( - Schedule.upTo({ times: 5 }), - Schedule.tap(({ attempt, output }) => Console.log(`attempt-${attempt}: ${Duration.toMillis(output)}ms`)) -) -``` - -Explanation: use `Schedule.tap` to observe inputs, outputs, and metadata such as -attempt number or selected duration without changing the schedule output. - -## Realistic Policies - -### Retry An HTTP Gateway With A Delay Envelope - -Goal: Create an HTTP gateway retry schedule. Retry only network failures, status -429, and status 500, 502, or 503. Use jittered exponential backoff starting at -100 milliseconds, cap selected delays at 2 seconds, allow at most 6 recurrences, -and output the selected delay. - -```ts -import { Duration, Effect, Schedule } from "effect" - -type GraphqlGatewayError = - | { readonly _tag: "Network" } - | { readonly _tag: "HttpStatus"; readonly status: number } - | { readonly _tag: "BadRequest" } - -const isRetryableGraphqlGatewayError = ( - error: GraphqlGatewayError -): boolean => - error._tag === "Network" || - (error._tag === "HttpStatus" && - (error.status === 429 || - error.status === 500 || - error.status === 502 || - error.status === 503)) - -const graphqlGatewayRetry = Schedule.max([ - Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.setInputType(), - Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, Duration.seconds(2)))) - ), - Schedule.recurs(6) -]).pipe( - Schedule.while(({ input }) => isRetryableGraphqlGatewayError(input)) -) -``` - -### Poll A Rollout With A Deadline - -Goal: Create a rollout watcher that starts from an aligned 1-second cadence, -jitters the selected delay, outputs the latest status, continues only while the -rollout is running, and stops after about 2 minutes. - -```ts -import { Duration, Schedule } from "effect" - -type RolloutStatus = { - readonly state: "running" | "succeeded" | "failed" -} - -const rolloutStatusWatcher = Schedule.fixed("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.jittered, - Schedule.while(({ input, elapsed }) => - input.state === "running" && - Duration.isLessThanOrEqualTo(Duration.millis(elapsed), Duration.minutes(2)) - ) -) -``` - -### Respect A Provider Retry-After Header - -Goal: Create a provider retry schedule. Retry status 429, 500, and 503. Use -exponential backoff starting at 1 second. When `retryAfter` is present, use it as -a lower bound. Cap selected delays at 1 minute, allow at most 6 recurrences, and -output the selected delay. - -```ts -import { Duration, Effect, Schedule } from "effect" - -type PushProviderResponse = { - readonly status: 429 | 500 | 503 | 400 - readonly retryAfter: Duration.Duration | undefined -} - -const pushNotificationProviderRetry = Schedule.max([ - Schedule.exponential("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.modifyDelay(({ output: response, duration }) => - Effect.succeed( - Duration.min( - response.retryAfter === undefined - ? duration - : Duration.max(duration, response.retryAfter), - Duration.minutes(1) - ) - ) - ) - ), - Schedule.recurs(6) -]).pipe( - Schedule.while(({ input }) => input.status === 429 || input.status === 500 || input.status === 503) -) -``` - -### Poll An OAuth Device Code Flow - -Goal: Create an OAuth device-code polling schedule. Poll every 5 seconds, add -another 5 seconds for `slow_down`, output the latest input, continue only for -`authorization_pending` and `slow_down`, and stop after about 15 minutes. - -```ts -import { Duration, Effect, Schedule } from "effect" - -type OAuthDeviceCodeStatus = { - readonly error: - | "authorization_pending" - | "slow_down" - | "access_denied" - | "expired_token" -} - -const oauthDeviceCodePolling = Schedule.spaced("5 seconds").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.addDelay(({ output: status }) => Effect.succeed(status.error === "slow_down" ? "5 seconds" : "0 millis")), - Schedule.while(({ input, elapsed }) => - (input.error === "authorization_pending" || input.error === "slow_down") && - Duration.isLessThanOrEqualTo(Duration.millis(elapsed), Duration.minutes(15)) - ) -) -``` - -### Escalate Incident Notifications In Phases - -Goal: Create an incident escalation cadence that emits 3 recurrences spaced 1 -minute apart, then 3 recurrences spaced 5 minutes apart, then switches to an -aligned 15-minute cadence. - -```ts -import { Schedule } from "effect" - -const incidentEscalationCadence = Schedule.spaced("1 minute").pipe( - Schedule.upTo({ times: 3 }), - Schedule.concat(Schedule.spaced("5 minutes").pipe(Schedule.upTo({ times: 3 }))), - Schedule.concat(Schedule.fixed("15 minutes")) -) -``` - -### Run Maintenance After A Warmup - -Goal: Create a maintenance schedule that performs one warmup recurrence after -about 30 seconds, then switches to a cron schedule that recurs every day at -03:00. - -```ts -import { Schedule } from "effect" - -const maintenanceCronAfterWarmup = Schedule.duration("30 seconds").pipe( - Schedule.concat(Schedule.cron("0 3 * * *")) -) -``` diff --git a/repos/effect/docker-compose.yaml b/repos/effect/docker-compose.yaml deleted file mode 100644 index 1a41e02f13..0000000000 --- a/repos/effect/docker-compose.yaml +++ /dev/null @@ -1,18 +0,0 @@ -services: - pg: - image: postgres:alpine # Using a lightweight Postgres image - environment: - POSTGRES_DB: effect_cluster - POSTGRES_USER: cluster - POSTGRES_PASSWORD: cluster - ports: - - "5432:5432" # Map host port 5432 to container port 5432 - volumes: - - db_data:/var/lib/postgresql/data # Persist data in a named volume - redis: - image: redis:alpine - ports: - - "6379:6379" - -volumes: - db_data: # Define the named volume diff --git a/repos/effect/dprint.json b/repos/effect/dprint.json index a47e1f94a0..7d1a121c60 100644 --- a/repos/effect/dprint.json +++ b/repos/effect/dprint.json @@ -18,13 +18,22 @@ "**/build", "**/docs", "**/coverage", - "packages/**/CHANGELOG.md", + "**/*.tsbuildinfo", + "**/CHANGELOG.md", "packages/effect/src/StandardSchema.ts", "!scratchpad/**/*", - ".changeset", - ".agents", - ".context", - ".specs" + "**/.*", + "!.agents", + "!.agents/**/*", + "!.changeset", + "!.changeset/**/*", + "!.envrc", + "!.github", + "!.github/**/*", + "!.gitignore", + "!.oxlintrc.json", + "!.vscode", + "!.vscode/**/*" ], "plugins": [ "https://plugins.dprint.dev/typescript-0.93.4.wasm", diff --git a/repos/effect/jsdocs.config.json b/repos/effect/jsdocs.config.json index f31bd337cc..6811e35b49 100644 --- a/repos/effect/jsdocs.config.json +++ b/repos/effect/jsdocs.config.json @@ -6,6 +6,7 @@ ], "exclude": [ "**/node_modules/**", + "packages/**/ai-docs/**", "packages/tools/**", "packages/**/src/index.ts", "packages/effect/src/StandardSchema.ts", diff --git a/repos/effect/migration/annotations/effect__Arbitrary.yaml b/repos/effect/migration/annotations/effect__Arbitrary.yaml index 8410a4a184..68be8ec479 100644 --- a/repos/effect/migration/annotations/effect__Arbitrary.yaml +++ b/repos/effect/migration/annotations/effect__Arbitrary.yaml @@ -1,17 +1,20 @@ +"effect/Arbitrary": + replacement: "effect/unstable/arbitrary/Arbitrary" + note: "Schema-derived generation moved to the native Arbitrary module. Effect no longer bridges to fast-check." "effect/Arbitrary#ArbitraryAnnotation": replacement: "Schema.Annotations.ToArbitrary.Declaration" - note: "Arbitrary derivation annotations now live in Schema.Annotations and use the toArbitrary key." + note: "Arbitrary derivation annotations now live in Schema.Annotations. Attach a toCodecArbitrary declaration callback that returns a Schema Link." "effect/Arbitrary#ArbitraryGenerationContext": - replacement: "Schema.Annotations.ToArbitrary.Context" - note: "Use the v4 arbitrary-derivation context type from Schema.Annotations." + replacement: "Schema.Annotations.ToArbitrary.DeclarationInput" + note: "Native arbitrary callbacks receive DeclarationInput with decoded type-parameter schemas and normalized constraints." "effect/Arbitrary#LazyArbitrary": - replacement: "Schema.Arbitrary" - note: "The arbitrary factory type moved onto Schema." + replacement: "effect/unstable/arbitrary/Arbitrary#Arbitrary" + note: "The generated-value description is now the native Arbitrary interface from effect/unstable/arbitrary." "effect/Arbitrary#make": - replacement: "Schema.toArbitrary" - note: "Arbitrary derivation is now exposed directly by Schema." - example: "Schema.toArbitrary(schema)(FastCheck)" + replacement: "effect/unstable/arbitrary/Arbitrary#schema" + note: "Derive a native Arbitrary from a Schema. Effect no longer bridges to fast-check." + example: "Arbitrary.schema(schema)" "effect/Arbitrary#makeLazy": - replacement: "Schema.toArbitrary" - note: "Lazy arbitrary derivation is now exposed directly by Schema." - example: "Schema.toArbitrary(schema)" + replacement: "effect/unstable/arbitrary/Arbitrary#schema" + note: "Lazy and eager Schema derivation are the same native Arbitrary.schema constructor." + example: "Arbitrary.schema(schema)" diff --git a/repos/effect/migration/annotations/effect__Channel.yaml b/repos/effect/migration/annotations/effect__Channel.yaml index 2b8ea573b0..a03978a159 100644 --- a/repos/effect/migration/annotations/effect__Channel.yaml +++ b/repos/effect/migration/annotations/effect__Channel.yaml @@ -173,11 +173,11 @@ replacement: "Channel.forever" note: "Use forever for infinite repetition. Channel.repeat takes a Schedule and may terminate, so it is not equivalent." "effect/Channel#run": - replacement: "Channel.runDone" - note: "Renamed to runDone for an inputless, outputless channel. Use runDrain if emitted elements should be discarded." + replacement: "Channel.runDrain" + note: "Use runDrain to consume all emitted elements and return the channel's done value." "effect/Channel#runScoped": replacement: "Channel.toPull" - note: "No direct scoped runner remains. Use toPull in the caller scope and recover Cause.Done; use runDone or runDrain when an internally managed scope is acceptable." + note: "No direct scoped runner remains. Use toPull in the caller scope and recover Cause.Done; use runDrain when an internally managed scope is acceptable." "effect/Channel#scopedWith": replacement: "Channel.unwrap" note: "Use Channel.unwrap(Effect.map(Effect.scope, (scope) => Channel.fromEffect(f(scope)))) so the effect uses the active channel scope." diff --git a/repos/effect/migration/annotations/effect__Config.yaml b/repos/effect/migration/annotations/effect__Config.yaml index 122eb53712..04c784718e 100644 --- a/repos/effect/migration/annotations/effect__Config.yaml +++ b/repos/effect/migration/annotations/effect__Config.yaml @@ -2,11 +2,11 @@ replacement: "Config.all" note: "Combine an iterable or record of Config values. A wholly absent product can use Config.withDefault or Config.option, while a partially supplied product fails." "effect/Config#array": - replacement: "Config.schema(Config.Array(valueSchema), path)" - note: "Array parsing is schema-based in v4; rebuild the element Config as a Schema and pass the optional path to Config.schema." + replacement: "Config.Array(valueSchema, path)" + note: "Array parsing is schema-based in v4; rebuild the element Config as a Schema and pass it with the optional path to Config.Array." "effect/Config#boolean": - replacement: "Config.boolean" - note: "Unchanged." + replacement: "Config.Boolean" + note: "Direct constructor rename." "effect/Config#branded": replacement: "Config.schema(schema.pipe(Schema.brand(brand)), path)" note: "Brand validation moved to Schema; define the branded schema and construct the Config with Config.schema." @@ -29,11 +29,11 @@ replacement: "Config.isConfig" note: "The Config marker is private in v4; use the public guard for runtime narrowing." "effect/Config#date": - replacement: "Config.date" - note: "Unchanged." + replacement: "Config.Date" + note: "Direct constructor rename." "effect/Config#duration": - replacement: "Config.duration" - note: "Unchanged." + replacement: "Config.Duration" + note: "Direct constructor rename." "effect/Config#fail": replacement: "Config.fail" note: "The v4 constructor takes a ConfigProvider.SourceError or Schema.SchemaError instead of a message; wrap the failure in the appropriate cause." @@ -44,47 +44,50 @@ replacement: "Config.schema(Schema.HashSet(valueSchema), path)" note: "HashSet parsing is schema-based in v4; replace the child Config with its value Schema." "effect/Config#integer": - replacement: "Config.int" - note: "Renamed to the shorter v4 integer constructor." + replacement: "Config.Int" + note: "Renamed to the shorter v4 integer constructor using the PascalCase constructor convention." "effect/Config#literal": - replacement: "Config.literals(literals, path)" - note: "The v3 curried variadic constructor became Config.literals with an array and inline path; use Config.literal for one value." + replacement: "Config.Literals(literals, path)" + note: "The v3 curried variadic constructor became Config.Literals with an array and inline path; use Config.Literal for one value." "effect/Config#LiteralValue": replacement: "SchemaAST.LiteralValue" note: "Use the literal value type shared by v4 Schema constructors." "effect/Config#logLevel": - replacement: "Config.logLevel" - note: "Unchanged." + replacement: "Config.LogLevel" + note: "Direct constructor rename." "effect/Config#mapAttempt": - replacement: "Config.mapOrFail" - note: "Catch exceptions explicitly and return an Effect failure containing Config.ConfigError; mapOrFail is Effect-based in v4." + replacement: "Config.mapEffect" + note: "Catch exceptions explicitly and return an Effect failure containing Config.ConfigError; mapEffect is Effect-based in v4." +"effect/Config#mapOrFail": + replacement: "Config.mapEffect" + note: "Renamed to match the effectful mapping convention used throughout the library." "effect/Config#nonEmptyString": - replacement: "Config.nonEmptyString" - note: "Unchanged." + replacement: "Config.NonEmptyString" + note: "Direct constructor rename." "effect/Config#number": - replacement: "Config.number" - note: "Unchanged; use Config.finite when NaN and infinities must be rejected." + replacement: "Config.Number" + note: "Direct constructor rename; use Config.Finite when NaN and infinities must be rejected." "effect/Config#orElseIf": replacement: "Config.orElse" note: "The fallback now receives Config.ConfigError; test it in the callback and re-fail with Config.fail(error.cause) when the predicate is false." "effect/Config#port": - replacement: "Config.port" - note: "Unchanged." + replacement: "Config.Port" + note: "Direct constructor rename." "effect/Config#primitive": replacement: "Config.schema(customSchema, path)" note: "Custom primitive parsing moved to Schema codecs; express decoding and diagnostics in a Schema, then pass it to Config.schema. Its canonical StringTree encoding must expose a concrete shape; opaque encodings such as Schema.Any or Schema.Unknown are not supported." "effect/Config#redacted": - replacement: "Config.redacted" + replacement: "Config.Redacted" note: "The string/path overload remains; replace the v3 Config argument overload with Config.map(config, Redacted.make)." "effect/Config#repeat": - replacement: "Config.schema(Config.Array(valueSchema), path)" - note: "Repeated values are represented by an array Schema in v4; Config.Array also accepts flat separated input." + replacement: "Config.Array(valueSchema, path)" + note: "Repeated values use the Config.Array constructor, which accepts structural arrays and flat separated input." "effect/Config#secret": - replacement: "Config.redacted" - note: "Secret was removed in favor of Redacted; this constructor already returns Redacted." + replacement: "Config.Redacted" + note: "Secret was removed in favor of Redacted; this constructor returns Redacted." "effect/Config#string": - replacement: "Config.string" - note: "Unchanged." + replacement: "Config.String" + note: "Direct constructor rename." "effect/Config#succeed": replacement: "Config.succeed" note: "Unchanged." @@ -95,8 +98,8 @@ replacement: "Config.succeed(undefined).pipe(Config.map(() => thunk()))" note: "The dedicated lazy constant constructor was removed; mapping a constant Config preserves evaluation at parse time." "effect/Config#url": - replacement: "Config.url" - note: "Unchanged." + replacement: "Config.URL" + note: "Direct constructor rename." "effect/Config#validate": replacement: "Config.schema(schema.check(check), path)" note: "Validation moved to Schema checks; attach the predicate and message to the Schema used by Config.schema." diff --git a/repos/effect/migration/annotations/effect__ConfigProvider.yaml b/repos/effect/migration/annotations/effect__ConfigProvider.yaml index 7da7c04bba..14bfddb15b 100644 --- a/repos/effect/migration/annotations/effect__ConfigProvider.yaml +++ b/repos/effect/migration/annotations/effect__ConfigProvider.yaml @@ -6,7 +6,7 @@ note: "Flat providers were removed; implement the unified path-based provider with ConfigProvider.make." "effect/ConfigProvider#ConfigProvider.FromEnvConfig": replacement: "Parameters[0]" - note: "Options are inline in v4 and contain env plus preserveEmptyStrings; custom path and sequence delimiters moved to provider path transforms and Config.Array/Config.Record schemas." + note: "Options are inline in v4 and contain env plus preserveEmptyStrings; custom path delimiters moved to provider path transforms, while separated sequences and records use Config.Array and Config.Record." "effect/ConfigProvider#ConfigProvider.FromMapConfig": replacement: "none" note: "fromMap and its delimiter options were removed; expand delimited keys into a nested value and use ConfigProvider.fromUnknown." diff --git a/repos/effect/migration/annotations/effect__Effect.yaml b/repos/effect/migration/annotations/effect__Effect.yaml index 4a7ee9816c..a70dcdb40c 100644 --- a/repos/effect/migration/annotations/effect__Effect.yaml +++ b/repos/effect/migration/annotations/effect__Effect.yaml @@ -528,7 +528,7 @@ effect/Effect#transposeMapOption: note: "Return `Effect.succeedNone` for None and map the Effect result to Some. Adapt arguments and imports to the v4 API." effect/Effect#try: replacement: "Effect.try" - note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." + note: "Use the callback overload for Cause.UnknownError, or the object overload with try and catch to map failures to a custom error. The callback-only overload does not accept a custom error type parameter." effect/Effect#tryMap: replacement: "Effect.flatMap + Effect.try" note: "FlatMap the source value into the v4 synchronous try constructor. Adapt arguments and imports to the v4 API." @@ -655,3 +655,9 @@ effect/Effect#zipLeft: effect/Effect#zipRight: replacement: "Effect.andThen" note: "Sequence the Effects and retain the second result. Adapt arguments and imports to the v4 API." +"effect/Effect#repeatOrElse": + replacement: "Effect.repeatOrElse" + note: "The fallback receives Option> instead of Option. Read metadata.output for the previous schedule output and metadata.attempt for the attempt count. The error also includes schedule failures." +"effect/Effect#tryPromise": + replacement: "Effect.tryPromise" + note: "Use the callback overload for Cause.UnknownError, or the object overload with try and catch to map failures to a custom error. The callback-only overload does not accept a custom error type parameter." diff --git a/repos/effect/migration/annotations/effect__Effectable.yaml b/repos/effect/migration/annotations/effect__Effectable.yaml index a90d6864a4..fe8066d276 100644 --- a/repos/effect/migration/annotations/effect__Effectable.yaml +++ b/repos/effect/migration/annotations/effect__Effectable.yaml @@ -3,7 +3,7 @@ note: "The public channel brand moved to its owning module; v4 uses a string TypeId rather than the v3 Symbol." "effect/Effectable#Class": replacement: "Effectable.Class" - note: "Still available; replace commit() with an override property or getter returning the Effect." + note: "Still available; replace commit() with an asEffect() method returning the Effect. The intermediate v4 override property/getter is no longer supported." "effect/Effectable#CommitPrimitive": replacement: "new() => Effect.Effect" note: "The named constructor interface was removed; inline the constructor type when needed." @@ -24,7 +24,7 @@ note: "The public stream brand moved to its owning module; v4 uses a string TypeId rather than the v3 Symbol." "effect/Effectable#StructuralClass": replacement: "Effectable.Class" - note: "Use Class and migrate commit() to override; v4 equality is structural by default." + note: "Use Class and migrate commit() to asEffect(); v4 equality is structural by default." "effect/Effectable#StructuralCommitPrototype": replacement: "Effectable.Prototype" note: "Use Prototype with evaluate; a separate structural prototype is unnecessary because v4 equality is structural by default." diff --git a/repos/effect/migration/annotations/effect__FastCheck.yaml b/repos/effect/migration/annotations/effect__FastCheck.yaml index 0a1868b0a2..8a0c421dd8 100644 --- a/repos/effect/migration/annotations/effect__FastCheck.yaml +++ b/repos/effect/migration/annotations/effect__FastCheck.yaml @@ -1,88 +1,91 @@ +"effect/FastCheck": + replacement: "fast-check" + note: "Effect no longer re-exports fast-check. Depend on the fast-check package and import it directly. For Schema-derived generation, use Arbitrary.schema from effect/unstable/arbitrary." "effect/FastCheck#ascii": replacement: "FastCheck.string" - note: "Import FastCheck from effect/testing. fast-check v4 replaced character arbitraries with string units." + note: "Depend on fast-check and import it directly. fast-check v4 replaced character arbitraries with string units." example: "FastCheck.string({ unit: \"binary-ascii\", minLength: 1, maxLength: 1 })" "effect/FastCheck#asciiString": replacement: "FastCheck.string" - note: "Import FastCheck from effect/testing. Use the binary-ascii string unit." + note: "Depend on fast-check and import it directly. Use the binary-ascii string unit." example: "FastCheck.string({ ...constraints, unit: \"binary-ascii\" })" "effect/FastCheck#base64": replacement: "FastCheck.constantFrom" - note: "Import FastCheck from effect/testing. Generate one base64 alphabet character; base64String remains for complete encoded strings." + note: "Depend on fast-check and import it directly. Generate one base64 alphabet character; base64String remains for complete encoded strings." example: "FastCheck.constantFrom(...\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/\")" "effect/FastCheck#bigIntN": replacement: "FastCheck.bigInt" - note: "Import FastCheck from effect/testing. Express the signed bit range with min and max constraints." + note: "Depend on fast-check and import it directly. Express the signed bit range with min and max constraints." "effect/FastCheck#bigUint": replacement: "FastCheck.bigInt" - note: "Import FastCheck from effect/testing. Use a minimum of 0n and the previous maximum." + note: "Depend on fast-check and import it directly. Use a minimum of 0n and the previous maximum." example: "FastCheck.bigInt({ min: 0n, max })" "effect/FastCheck#BigUintConstraints": replacement: "FastCheck.BigIntConstraints" - note: "Import FastCheck from effect/testing. Unsigned bigint constraints were consolidated into BigIntConstraints with min: 0n." + note: "Depend on fast-check and import it directly. Unsigned bigint constraints were consolidated into BigIntConstraints with min: 0n." "effect/FastCheck#bigUintN": replacement: "FastCheck.bigInt" - note: "Import FastCheck from effect/testing. Express the unsigned bit range with min and max constraints." + note: "Depend on fast-check and import it directly. Express the unsigned bit range with min and max constraints." example: "FastCheck.bigInt({ min: 0n, max: (1n << BigInt(n)) - 1n })" "effect/FastCheck#char": replacement: "FastCheck.string" - note: "Import FastCheck from effect/testing. Use a one-unit printable ASCII string." + note: "Depend on fast-check and import it directly. Use a one-unit printable ASCII string." example: "FastCheck.string({ unit: \"grapheme-ascii\", minLength: 1, maxLength: 1 })" "effect/FastCheck#char16bits": replacement: "FastCheck.nat" - note: "Import FastCheck from effect/testing. Map a 16-bit natural number through String.fromCharCode." + note: "Depend on fast-check and import it directly. Map a 16-bit natural number through String.fromCharCode." example: "FastCheck.nat({ max: 0xffff }).map(String.fromCharCode)" "effect/FastCheck#check": replacement: "FastCheck.check" - note: "Import FastCheck from effect/testing. The runner remains, but RunDetails.error was replaced by errorInstance in fast-check v4." + note: "Depend on fast-check and import it directly. The runner remains, but RunDetails.error was replaced by errorInstance in fast-check v4." "effect/FastCheck#constant": replacement: "FastCheck.constant" - note: "Import FastCheck from effect/testing. The API remains; v4 infers literal types by default." + note: "Depend on fast-check and import it directly. The API remains; v4 infers literal types by default." "effect/FastCheck#context": replacement: "FastCheck.context" - note: "Import FastCheck from effect/testing. The API is otherwise unchanged." + note: "Depend on fast-check and import it directly. The API is otherwise unchanged." "effect/FastCheck#fullUnicode": replacement: "FastCheck.string" - note: "Import FastCheck from effect/testing. Use a one-unit binary Unicode string." + note: "Depend on fast-check and import it directly. Use a one-unit binary Unicode string." example: "FastCheck.string({ unit: \"binary\", minLength: 1, maxLength: 1 })" "effect/FastCheck#fullUnicodeString": replacement: "FastCheck.string" - note: "Import FastCheck from effect/testing. Use the binary string unit." + note: "Depend on fast-check and import it directly. Use the binary string unit." example: "FastCheck.string({ ...constraints, unit: \"binary\" })" "effect/FastCheck#hexa": replacement: "FastCheck.integer" - note: "Import FastCheck from effect/testing. Map an integer from 0 through 15 to a hexadecimal character." + note: "Depend on fast-check and import it directly. Map an integer from 0 through 15 to a hexadecimal character." "effect/FastCheck#hexaString": replacement: "FastCheck.string" - note: "Import FastCheck from effect/testing. Pass a hexadecimal-character arbitrary as the string unit." + note: "Depend on fast-check and import it directly. Pass a hexadecimal-character arbitrary as the string unit." "effect/FastCheck#stream": replacement: "FastCheck.stream" - note: "Import FastCheck from effect/testing. The API remains; update custom generator and Random implementations for fast-check v4 typings." + note: "Depend on fast-check and import it directly. The API remains; update custom generator and Random implementations for fast-check v4 typings." "effect/FastCheck#string16bits": replacement: "FastCheck.string" - note: "Import FastCheck from effect/testing. Pass a char16bits-compatible arbitrary as the string unit." + note: "Depend on fast-check and import it directly. Pass a char16bits-compatible arbitrary as the string unit." "effect/FastCheck#stringOf": replacement: "FastCheck.string" - note: "Import FastCheck from effect/testing. Pass the former character arbitrary as the unit constraint." + note: "Depend on fast-check and import it directly. Pass the former character arbitrary as the unit constraint." example: "FastCheck.string({ ...constraints, unit: arbitrary })" "effect/FastCheck#unicode": replacement: "FastCheck.integer" - note: "Import FastCheck from effect/testing. Map BMP code points while excluding surrogate code points; prefer the binary string unit for full Unicode." + note: "Depend on fast-check and import it directly. Map BMP code points while excluding surrogate code points; prefer the binary string unit for full Unicode." "effect/FastCheck#unicodeJson": replacement: "FastCheck.json" - note: "Import FastCheck from effect/testing. Select binary or grapheme strings with stringUnit." + note: "Depend on fast-check and import it directly. Select binary or grapheme strings with stringUnit." example: "FastCheck.json({ stringUnit: \"binary\" })" "effect/FastCheck#UnicodeJsonSharedConstraints": replacement: "FastCheck.JsonSharedConstraints" - note: "Import FastCheck from effect/testing. Unicode JSON generation was consolidated into JsonSharedConstraints.stringUnit." + note: "Depend on fast-check and import it directly. Unicode JSON generation was consolidated into JsonSharedConstraints.stringUnit." "effect/FastCheck#unicodeJsonValue": replacement: "FastCheck.jsonValue" - note: "Import FastCheck from effect/testing. Select binary or grapheme strings with stringUnit." + note: "Depend on fast-check and import it directly. Select binary or grapheme strings with stringUnit." example: "FastCheck.jsonValue({ stringUnit: \"binary\" })" "effect/FastCheck#unicodeString": replacement: "FastCheck.string" - note: "Import FastCheck from effect/testing. Pass a BMP-code-point arbitrary as the unit constraint; prefer unit: binary for full Unicode." + note: "Depend on fast-check and import it directly. Pass a BMP-code-point arbitrary as the unit constraint; prefer unit: binary for full Unicode." "effect/FastCheck#uuidV": replacement: "FastCheck.uuid" - note: "Import FastCheck from effect/testing. Specify the UUID version through constraints." + note: "Depend on fast-check and import it directly. Specify the UUID version through constraints." example: "FastCheck.uuid({ version: 4 })" diff --git a/repos/effect/migration/annotations/effect__Graph.yaml b/repos/effect/migration/annotations/effect__Graph.yaml index 4b1997f9da..1f58ee52e2 100644 --- a/repos/effect/migration/annotations/effect__Graph.yaml +++ b/repos/effect/migration/annotations/effect__Graph.yaml @@ -6,10 +6,10 @@ note: "The immutable type remains, but storage is opaque; replace field access with Graph nodes, edges, count, lookup, neighbor, and acyclicity APIs." "effect/Graph#MutableGraph": replacement: "Graph.MutableGraph" - note: "The mutable type remains but no longer extends Graph.Proto; obtain it through Graph.mutate or Graph.beginMutation and use public mutation/query functions." + note: "The mutable type remains but no longer shares a public base interface with immutable Graph; obtain it through Graph.mutate or Graph.beginMutation and use public mutation/query functions." "effect/Graph#Proto": - replacement: "Graph.Proto" - note: "The name remains as the opaque immutable graph protocol; it no longer exposes storage and is no longer the base of MutableGraph." + replacement: "none" + note: "The common graph protocol was removed. Use Graph.Graph or Graph.MutableGraph as appropriate and replace storage-field access with public graph query and mutation functions." "effect/Graph#SearchConfig": replacement: "Graph.SearchConfig" note: "The type remains; direction is now Graph.TraversalDirection and also accepts undirected, while radius limits traversal depth." diff --git a/repos/effect/migration/annotations/effect__Layer.yaml b/repos/effect/migration/annotations/effect__Layer.yaml index def772ee9b..925c52e312 100644 --- a/repos/effect/migration/annotations/effect__Layer.yaml +++ b/repos/effect/migration/annotations/effect__Layer.yaml @@ -168,7 +168,7 @@ note: "No version-mismatch log-level Reference or public replacement exists." "effect/Layer#tapErrorCause": replacement: "Layer.tapCause" - note: "The cause observer was renamed." + note: "The cause observer was renamed. Its callback must accept the source layer's full error cause; a callback narrowed to only part of the error union is rejected." "effect/Layer#toRuntime": replacement: "Layer.build(self), then Effect.runForkWith, Effect.runPromiseWith, or Effect.runSyncWith" note: "Runtime was removed; build a Context, or use ManagedRuntime.make for a reusable managed runner." @@ -187,3 +187,6 @@ "effect/Layer#zipWith": replacement: "Layer.fromBuild with concurrent Effect.zipWith over Layer.buildWithMemoMap" note: "Combine acquisition effects directly; use Layer.merge when the function only merged Context values." +"effect/Layer#tapError": + replacement: "Layer.tapError" + note: "The observer must accept the source layer's full error union. Widen callbacks that handled only a subtype; saved curried observers preserve the source error type." diff --git a/repos/effect/migration/annotations/effect__LayerMap.yaml b/repos/effect/migration/annotations/effect__LayerMap.yaml index a87d5e2f54..12e9863535 100644 --- a/repos/effect/migration/annotations/effect__LayerMap.yaml +++ b/repos/effect/migration/annotations/effect__LayerMap.yaml @@ -3,7 +3,7 @@ note: "The type remains; runtime(key) became contextEffect(key) and returns Context." "effect/LayerMap#Service": replacement: "LayerMap.Service" - note: "Use layer instead of Default, layerNoDeps instead of DefaultWithoutDependencies, and contextEffect instead of runtime." + note: "Use layer instead of Default, layerNoDeps instead of DefaultWithoutDependencies, and contextEffect instead of runtime. Preloading does not remove acquisition errors from later lookups, which can reacquire expired or invalidated entries." "effect/LayerMap#Service.Context": replacement: "LayerMap.Service.Services" note: "The input-services extractor was renamed." diff --git a/repos/effect/migration/annotations/effect__Logger.yaml b/repos/effect/migration/annotations/effect__Logger.yaml index b8a4a8cf45..77a9957fb5 100644 --- a/repos/effect/migration/annotations/effect__Logger.yaml +++ b/repos/effect/migration/annotations/effect__Logger.yaml @@ -54,7 +54,7 @@ effect/Logger#pretty: note: "Logger.layer replaces the active set; include tracerLogger to preserve v3 built-in layer behavior." effect/Logger#prettyLogger: replacement: "Logger.consolePretty" - note: "Direct constructor rename; call it with the same options." + note: "Renamed to consolePretty. Remove the stderr option; provide Logger.LogToStderr with true to route TTY output to console.error. Colors, formatDate, and mode remain constructor options." effect/Logger#prettyLoggerDefault: replacement: "Logger.consolePretty()" note: "The prebuilt singleton became a constructor call." @@ -95,8 +95,8 @@ effect/Logger#withMinimumLogLevel: replacement: "Effect.provideService(effect, References.MinimumLogLevel, level)" note: "Replace the FiberRef-local helper with reference provisioning." effect/Logger#withSpanAnnotations: - replacement: "custom Logger.make wrapper using options.fiber.currentSpan" - note: "No transparent generic equivalent remains. Read span identity from options.fiber.currentSpan and add it to custom output as needed." + replacement: "custom Logger.make wrapper using options.fiber.cache.span" + note: "No transparent generic equivalent remains. Read span identity from options.fiber.cache.span and add it to custom output as needed." effect/Logger#zip: replacement: "Logger.make(options => [left.log(options), right.log(options)])" note: "No named combinator remains; invoke both loggers and return their output tuple." diff --git a/repos/effect/migration/annotations/effect__ParseResult.yaml b/repos/effect/migration/annotations/effect__ParseResult.yaml index 3feefd4736..72c6479214 100644 --- a/repos/effect/migration/annotations/effect__ParseResult.yaml +++ b/repos/effect/migration/annotations/effect__ParseResult.yaml @@ -8,6 +8,9 @@ "effect/ParseResult#ArrayFormatterIssue": replacement: "StandardSchemaV1.FailureResult[\"issues\"][number]" note: "Use the Standard Schema issue shape returned by makeFormatterStandardSchemaV1." +"effect/ParseResult#Composite": + replacement: "SchemaIssue.Composite" + note: "Composite parse failures moved to SchemaIssue. The v4 constructor takes the failing AST and an array of nested issues; input is retained only when reportInput is enabled." "effect/ParseResult#DeclarationDecodeUnknown": replacement: "SchemaGetter.Getter" note: "Custom declaration decoding now uses SchemaGetter values and Schema.declare annotations." @@ -68,6 +71,9 @@ "effect/ParseResult#orElse": replacement: "Effect.orElse" note: "Schema transformations now use Effect combinators." +"effect/ParseResult#Pointer": + replacement: "SchemaIssue.Pointer" + note: "Path-qualified failures moved to SchemaIssue. Construct them with the property path and nested issue; rejected input belongs to the nested issue when reportInput is enabled." "effect/ParseResult#parseError": replacement: "Schema.SchemaError" note: "Construct a SchemaError from a SchemaIssue.Issue." @@ -94,6 +100,9 @@ replacement: "SchemaIssue.defaultFormatter" note: "Use the default SchemaIssue string formatter." example: "SchemaIssue.defaultFormatter(issue)" +"effect/ParseResult#Transformation": + replacement: "SchemaIssue.Encoding" + note: "Transformation-stage failures are Encoding issues in v4. They retain the failing AST and nested issue; the old Encoded, Transformation, and Type kind discriminator was removed." "effect/ParseResult#try": replacement: "Effect.try" note: "Schema transformations now use Effect and map thrown errors to SchemaIssue values." diff --git a/repos/effect/migration/annotations/effect__Schema.yaml b/repos/effect/migration/annotations/effect__Schema.yaml index 69c83c30e4..d3daa60e17 100644 --- a/repos/effect/migration/annotations/effect__Schema.yaml +++ b/repos/effect/migration/annotations/effect__Schema.yaml @@ -1155,10 +1155,10 @@ effect/Schema#TaggedStruct: note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. effect/Schema#TemplateLiteral: replacement: Schema.TemplateLiteral(parts) - note: Pass template literal parts as one array. + note: Pass template literal parts as one array. Parts must not contain encodings, including inside unions and nested templates. Transformations whose decoded and encoded types are equal are also rejected. Use Schema.TemplateLiteralParser(parts) for transformed parts. effect/Schema#TemplateLiteralParser: - replacement: Schema.TemplateLiteralParser(schema.parts) - note: Create the template schema first and pass its `parts` property. + replacement: Schema.TemplateLiteralParser(parts) + note: Pass template literal parts directly as one array. Transformed parts are supported, and their decoding and encoding services are required in the corresponding direction. effect/Schema#TimeZone: replacement: Schema.TimeZoneFromString note: Use the string codec; v4 `TimeZone` is the self schema. diff --git a/repos/effect/migration/annotations/effect__SchemaAST.yaml b/repos/effect/migration/annotations/effect__SchemaAST.yaml index 00a8f970d4..6627326015 100644 --- a/repos/effect/migration/annotations/effect__SchemaAST.yaml +++ b/repos/effect/migration/annotations/effect__SchemaAST.yaml @@ -1,6 +1,6 @@ "effect/SchemaAST#Annotated": - replacement: "SchemaAST.Base" - note: "All v4 AST nodes extend Base, which owns annotations, checks, encoding, and context." + replacement: "SchemaAST.AST" + note: "The public base type was removed. Use the AST union; every variant still exposes annotations, checks, encoding, and context." "effect/SchemaAST#annotations": replacement: "SchemaAST.annotate" note: "Use the v4 annotation helper and string-keyed Schema.Annotations." @@ -12,7 +12,7 @@ note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." "effect/SchemaAST#ArbitraryAnnotationId": replacement: "Schema.Annotations.ToArbitrary" - note: "Symbol annotation IDs were removed; use the toArbitrary annotation key and its Schema.Annotations types." + note: "Symbol annotation IDs were removed. Declarations use the toCodecArbitrary annotation; filters use arbitraryConstraint." "effect/SchemaAST#AST": replacement: "SchemaAST.AST" note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." @@ -43,6 +43,9 @@ "effect/SchemaAST#Compiler": replacement: "none" note: "The generic AST compiler abstraction was removed; traverse the discriminated SchemaAST.AST union directly or use a higher-level Schema derivation API." +"effect/SchemaAST#pick": + replacement: "none" + note: "The low-level AST picker was removed. Keep field selection at the Schema.Struct level with mapFields and Struct.pick, or discriminate and rebuild custom AST nodes explicitly." "effect/SchemaAST#composeTransformation": replacement: "SchemaAST.Encoding" note: "V4 transformations are SchemaAST.Link values in an encoding chain; compose by adding links with SchemaAST.decodeTo." @@ -378,7 +381,7 @@ note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." "effect/SchemaAST#TemplateLiteral": replacement: "SchemaAST.TemplateLiteral" - note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. Parts must not contain encodings, including inside unions or nested templates; use Schema.TemplateLiteralParser for transformed parts." "effect/SchemaAST#TemplateLiteralSpan": replacement: "SchemaAST.TemplateLiteral" note: "Template literal parts are represented directly as AST values in v4." diff --git a/repos/effect/migration/annotations/effect__SortedSet.yaml b/repos/effect/migration/annotations/effect__SortedSet.yaml index cc0e6e0c88..7e95f864c3 100644 --- a/repos/effect/migration/annotations/effect__SortedSet.yaml +++ b/repos/effect/migration/annotations/effect__SortedSet.yaml @@ -58,3 +58,6 @@ "effect/SortedSet#some": replacement: "HashSet.some" note: "Run the predicate against the replacement HashSet; sort first only if traversal order has observable effects." +"effect/SortedSet#toggle": + replacement: "HashSet.has + HashSet.add / HashSet.remove" + note: "HashSet has no toggle; branch on membership and add or remove the element." diff --git a/repos/effect/migration/annotations/effect__SynchronizedRef.yaml b/repos/effect/migration/annotations/effect__SynchronizedRef.yaml index 5fef8a0505..37db3e7baa 100644 --- a/repos/effect/migration/annotations/effect__SynchronizedRef.yaml +++ b/repos/effect/migration/annotations/effect__SynchronizedRef.yaml @@ -1,9 +1,9 @@ effect/SynchronizedRef#SynchronizedRef: replacement: "SynchronizedRef.SynchronizedRef" - note: "The model remains, now extends the v4 Ref model, and is read or updated through explicit SynchronizedRef operations." + note: "The model remains but no longer extends Ref; read and update it through explicit SynchronizedRef operations. The curried v4 modifySomeEffect takes only the callback, which returns an Effect of [result, Option]; remove the v3 fallback and outer Option." effect/SynchronizedRef#SynchronizedRef.Variance: - replacement: "Ref.Ref.Variance" - note: "SynchronizedRef now inherits Ref variance instead of declaring a separate public variance marker." + replacement: "none" + note: "The public nested variance marker was removed. SynchronizedRef uses an internal brand in v4, so do not refer to a variance interface directly." effect/SynchronizedRef#SynchronizedRefTypeId: replacement: "none" note: "The SynchronizedRef type id is internal in v4; do not inspect or construct the brand directly." diff --git a/repos/effect/migration/annotations/effect__TestConfig.yaml b/repos/effect/migration/annotations/effect__TestConfig.yaml index 39470247c5..18ddf7d6d2 100644 --- a/repos/effect/migration/annotations/effect__TestConfig.yaml +++ b/repos/effect/migration/annotations/effect__TestConfig.yaml @@ -3,4 +3,4 @@ note: "The v3 constructor only returned its parameter object. The TestConfig service was removed; keep a plain object only for application-owned configuration." "effect/TestConfig#TestConfig": replacement: "none" - note: "There is no v4 TestConfig service. Move runner settings to Vitest and FastCheck options, or define an application-specific Context.Reference if runtime access is needed." + note: "There is no v4 TestConfig service. Move runner settings to Vitest and native Arbitrary check options, or define an application-specific Context.Reference if runtime access is needed." diff --git a/repos/effect/migration/annotations/effect__TestServices.yaml b/repos/effect/migration/annotations/effect__TestServices.yaml index 2310757346..9ab4fe3dfc 100644 --- a/repos/effect/migration/annotations/effect__TestServices.yaml +++ b/repos/effect/migration/annotations/effect__TestServices.yaml @@ -47,11 +47,11 @@ replacement: "Vitest TestOptions.retry" note: "Configure retry in Vitest test options. To retry an Effect inside a test, use Effect.retry." "effect/TestServices#samples": - replacement: "{ fastCheck: { numRuns } }" - note: "Pass the run count through @effect/vitest property-test options, for example it.effect.prop(..., { fastCheck: { numRuns: samples } })." + replacement: "{ arbitrary: { runs } }" + note: "Pass the run count through @effect/vitest property-test options, for example it.effect.prop(..., { arbitrary: { runs: samples } })." "effect/TestServices#shrinks": - replacement: "none" - note: "The legacy maximum-shrinks service setting was removed; @effect/vitest forwards FastCheck.Parameters, which has no equivalent service value." + replacement: "{ arbitrary: { maxShrinks } }" + note: "Pass maxShrinks through @effect/vitest property-test options, for example it.effect.prop(..., { arbitrary: { maxShrinks } })." "effect/TestServices#size": replacement: "CurrentSize" note: "Define a custom Context.Reference and yield it to read the current size." @@ -66,19 +66,19 @@ note: "Use the custom reference's callback, or preferably yield CurrentSize in Effect.gen." "effect/TestServices#testConfig": replacement: "none" - note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference." + note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test arbitrary options; model application state as a custom Context.Reference." "effect/TestServices#testConfigLayer": replacement: "none" - note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference." + note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test arbitrary options; model application state as a custom Context.Reference." "effect/TestServices#testConfigWith": replacement: "none" - note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference." + note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test arbitrary options; model application state as a custom Context.Reference." "effect/TestServices#withTestConfig": replacement: "none" - note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference." + note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test arbitrary options; model application state as a custom Context.Reference." "effect/TestServices#withTestConfigScoped": replacement: "none" - note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference." + note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test arbitrary options; model application state as a custom Context.Reference." "effect/TestServices#TestServices": replacement: "TestClock.TestClock | TestConsole.TestConsole" note: "This is the v4 @effect/vitest test-environment union. it.effect provides both automatically; it.live provides neither override." diff --git a/repos/effect/migration/annotations/effect__ai__AiError.yaml b/repos/effect/migration/annotations/effect__ai__AiError.yaml index d906cf83a4..34d041d504 100644 --- a/repos/effect/migration/annotations/effect__ai__AiError.yaml +++ b/repos/effect/migration/annotations/effect__ai__AiError.yaml @@ -19,3 +19,6 @@ "@effect/ai/AiError#UnknownError": replacement: "AiError.make + AiError.UnknownError" note: "UnknownError is now a semantic reason rather than a top-level error. Put module and method on AiError.make and inspect reason._tag when handling the outer AiError." +"@effect/ai/AiError#HttpRequestDetails": + replacement: "AiError.HttpRequestDetails" + note: "Retained in effect/unstable/ai/AiError and also re-exported as Response.HttpRequestDetails. Hash is an optional string instead of Option, headers may contain Redacted strings, and method includes TRACE." diff --git a/repos/effect/migration/annotations/effect__ai__Response.yaml b/repos/effect/migration/annotations/effect__ai__Response.yaml index 2a1351c128..886befa160 100644 --- a/repos/effect/migration/annotations/effect__ai__Response.yaml +++ b/repos/effect/migration/annotations/effect__ai__Response.yaml @@ -67,3 +67,6 @@ "@effect/ai/Response#urlSourcePart": replacement: "Response.makePart(\"source\", { ...params, sourceType: \"url\" })" note: "The lowercase convenience constructor was removed. The UrlSourcePart model remains, and the generic constructor now requires the URL source discriminator." +"@effect/ai/Response#ToolResultPart": + replacement: "Response.ToolResultPart" + note: "The schema selects success or failure using isFailure rather than trying both result schemas. It returns Schema.Codec; supply decoding services when decoding and encoding services when encoding." diff --git a/repos/effect/migration/annotations/effect__ai__Tool.yaml b/repos/effect/migration/annotations/effect__ai__Tool.yaml index 5bd6608419..e0fefb0440 100644 --- a/repos/effect/migration/annotations/effect__ai__Tool.yaml +++ b/repos/effect/migration/annotations/effect__ai__Tool.yaml @@ -55,3 +55,9 @@ "@effect/ai/Tool#TypeId": replacement: "Tool.TypeId" note: "Moved to effect/unstable/ai/Tool and remains public. Its literal changed, so use the export rather than retaining the old hard-coded string." +"@effect/ai/Tool#Result": + replacement: "Tool.Result" + note: "The result includes success, declared failure, and execution-denied or execution-interrupted values in both failure modes. Return mode also includes AiError; handle these variants when narrowing results." +"@effect/ai/Tool#ResultEncoded": + replacement: "Tool.ResultEncoded" + note: "The encoded result includes execution-denied and execution-interrupted variants in both failure modes, plus encoded AiError in return mode." diff --git a/repos/effect/migration/annotations/effect__cli__Args.yaml b/repos/effect/migration/annotations/effect__cli__Args.yaml index f95db093c2..c07e2b7fe0 100644 --- a/repos/effect/migration/annotations/effect__cli__Args.yaml +++ b/repos/effect/migration/annotations/effect__cli__Args.yaml @@ -15,9 +15,9 @@ note: "Argument constructors now take the name as a required first parameter." "@effect/cli/Args#Args.FormatArgsConfig": replacement: "Primitive.FileParseOptions" - note: "Pass the name separately and use the format option with Argument.fileParse or Argument.fileSchema." + note: "Pass the name separately and use the format option with Argument.FileParse or Argument.FileSchema." "@effect/cli/Args#Args.PathArgsConfig": - replacement: "Argument.path(name, { pathType, mustExist })" + replacement: "Argument.Path(name, { pathType, mustExist })" note: "Path options are inline; map exists=yes to mustExist=true and either to omission. exists=no has no exact replacement." "@effect/cli/Args#Args.Variance": replacement: "Argument.Argument" @@ -35,11 +35,35 @@ replacement: "Argument.between" note: "Use the moved combinator; v4 validates bounds when constructing the parameter." "@effect/cli/Args#boolean": - replacement: "Flag.boolean / Argument.choiceWithValue" + replacement: "Flag.Boolean / Argument.ChoiceWithValue" note: "Positional booleans were removed as ambiguous; prefer a boolean flag or explicit true/false positional choices." +"@effect/cli/Args#choice": + replacement: "Argument.Literals" + note: "Use the renamed constructor and pass the argument name explicitly." +"@effect/cli/Args#date": + replacement: "Argument.Date" + note: "Use the renamed constructor and pass the argument name explicitly." +"@effect/cli/Args#directory": + replacement: "Argument.Directory" + note: "Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement." +"@effect/cli/Args#file": + replacement: "Argument.File" + note: "Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement." "@effect/cli/Args#fileContent": - replacement: "Argument.file + Argument.mapEffect" + replacement: "Argument.File + Argument.mapEffect" note: "Parse a path and read it with FileSystem.readFile; no binary-content argument constructor remains." +"@effect/cli/Args#fileParse": + replacement: "Argument.FileParse" + note: "Pass the old format as an options field; v4 returns parsed content rather than a path/content tuple." +"@effect/cli/Args#fileSchema": + replacement: "Argument.FileSchema" + note: "Pass the old format as an options field and use a v4 Schema constraint decoder." +"@effect/cli/Args#fileText": + replacement: "Argument.FileText" + note: "Use the renamed constructor; it returns content only." +"@effect/cli/Args#float": + replacement: "Argument.Finite" + note: "Use the renamed constructor; it rejects non-finite numbers." "@effect/cli/Args#getHelp": replacement: "none" note: "Per-argument help introspection was removed; Command generates help internally." @@ -55,24 +79,36 @@ "@effect/cli/Args#getUsage": replacement: "none" note: "The public Usage tree was removed; Command generates a usage string internally." +"@effect/cli/Args#integer": + replacement: "Argument.Int" + note: "Use the renamed constructor and pass the argument name explicitly." "@effect/cli/Args#isArgs": replacement: "Param.isParam(value) && value.kind === Param.argumentKind" note: "Arguments now use the shared Param representation and an explicit kind discriminator." "@effect/cli/Args#map": replacement: "Argument.map" note: "Use the moved combinator." +"@effect/cli/Args#none": + replacement: "omit the config entry" + note: "V4 Argument.Never is an always-failing sentinel, not v3's empty successful argument set." "@effect/cli/Args#optional": replacement: "Argument.optional" note: "Use the moved combinator; it still returns Option." +"@effect/cli/Args#path": + replacement: "Argument.Path" + note: "Path options are inline; map exists=yes to mustExist=true and either to omission. exists=no has no exact replacement." +"@effect/cli/Args#redacted": + replacement: "Argument.Redacted" + note: "Use the renamed constructor and pass the argument name explicitly." "@effect/cli/Args#repeated": replacement: "Argument.variadic" note: "Renamed to variadic; pass optional min and max bounds." "@effect/cli/Args#secret": - replacement: "Argument.redacted" + replacement: "Argument.Redacted" note: "Use Redacted-backed positional input." "@effect/cli/Args#text": - replacement: "Argument.string" - note: "Renamed to string; pass the argument name explicitly." + replacement: "Argument.String" + note: "Renamed to String; pass the argument name explicitly." "@effect/cli/Args#validate": replacement: "argument.parse({ flags: {}, arguments: args })" note: "Parsing is now a Param method and returns leftover tokens with the value; errors are CliError." diff --git a/repos/effect/migration/annotations/effect__cli__CommandDirective.yaml b/repos/effect/migration/annotations/effect__cli__CommandDirective.yaml index 3c157a9c82..8dc872ab75 100644 --- a/repos/effect/migration/annotations/effect__cli__CommandDirective.yaml +++ b/repos/effect/migration/annotations/effect__cli__CommandDirective.yaml @@ -1,5 +1,5 @@ "@effect/cli/CommandDirective#builtIn": - replacement: "GlobalFlag.action" + replacement: "GlobalFlag.Action" note: "Define a custom action flag; v4 runners no longer return built-in directives." "@effect/cli/CommandDirective#BuiltIn": replacement: "GlobalFlag.Action" diff --git a/repos/effect/migration/annotations/effect__cli__Options.yaml b/repos/effect/migration/annotations/effect__cli__Options.yaml index 22a76c724d..966f792e3f 100644 --- a/repos/effect/migration/annotations/effect__cli__Options.yaml +++ b/repos/effect/migration/annotations/effect__cli__Options.yaml @@ -17,40 +17,40 @@ replacement: "Flag.between" note: "Use the moved combinator; v4 validates bounds when constructing the parameter." "@effect/cli/Options#boolean": - replacement: "Flag.boolean + Flag.withDefault" - note: "Use Flag.boolean(name).pipe(Flag.withDefault(false)) to preserve v3's omitted-flag default; bare Flag.boolean is now required. --no-name is automatic and aliases are added with Flag.withAlias." + replacement: "Flag.Boolean + Flag.withDefault" + note: "Use Flag.Boolean(name).pipe(Flag.withDefault(false)) to preserve v3's omitted-flag default; bare Flag.Boolean is now required. --no-name is automatic and aliases are added with Flag.withAlias." "@effect/cli/Options#choice": - replacement: "Flag.choice" + replacement: "Flag.Literals" note: "Use the moved constructor." "@effect/cli/Options#choiceWithValue": - replacement: "Flag.choiceWithValue" + replacement: "Flag.ChoiceWithValue" note: "Use the moved constructor." "@effect/cli/Options#date": - replacement: "Flag.date" + replacement: "Flag.Date" note: "Use the moved constructor." "@effect/cli/Options#directory": - replacement: "Flag.directory" + replacement: "Flag.Directory" note: "Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement." "@effect/cli/Options#file": - replacement: "Flag.file" + replacement: "Flag.File" note: "Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement." "@effect/cli/Options#fileContent": - replacement: "Flag.file + Flag.mapEffect" + replacement: "Flag.File + Flag.mapEffect" note: "Parse a path and read it with FileSystem.readFile; no binary-content flag constructor remains." "@effect/cli/Options#fileParse": - replacement: "Flag.fileParse" + replacement: "Flag.FileParse" note: "Pass the old format as an options field; v4 returns parsed content rather than a path/content tuple." "@effect/cli/Options#fileSchema": - replacement: "Flag.fileSchema" + replacement: "Flag.FileSchema" note: "Pass the old format as an options field and use a v4 Schema constraint decoder." "@effect/cli/Options#fileText": - replacement: "Flag.file + Flag.mapEffect" - note: "Flag.fileText returns content only; read after Flag.file when the path/content tuple must be preserved." + replacement: "Flag.File + Flag.mapEffect" + note: "Flag.FileText returns content only; read after Flag.File when the path/content tuple must be preserved." "@effect/cli/Options#filterMap": replacement: "Flag.filterMap" note: "Use the moved combinator and replace the fixed message with an onNone function." "@effect/cli/Options#float": - replacement: "Flag.float" + replacement: "Flag.Finite" note: "Use the moved constructor." "@effect/cli/Options#getHelp": replacement: "none" @@ -62,7 +62,7 @@ replacement: "none" note: "The public Usage tree was removed; Command generates a usage string internally." "@effect/cli/Options#integer": - replacement: "Flag.integer" + replacement: "Flag.Int" note: "Use the moved constructor." "@effect/cli/Options#isBool": replacement: "none" @@ -71,7 +71,7 @@ replacement: "Param.isParam(value) && value.kind === Param.flagKind" note: "Flags now use the shared Param representation and an explicit kind discriminator." "@effect/cli/Options#keyValueMap": - replacement: "Flag.keyValuePair" + replacement: "Flag.KeyValuePair" note: "Renamed and now returns Record rather than HashMap." "@effect/cli/Options#map": replacement: "Flag.map" @@ -84,7 +84,7 @@ note: "Use the moved combinator; onError now returns a string rather than HelpDoc." "@effect/cli/Options#none": replacement: "omit the config entry" - note: "V4 Flag.none is an always-failing sentinel, not v3's empty successful option set." + note: "V4 Flag.Never is an always-failing sentinel, not v3's empty successful option set." "@effect/cli/Options#optional": replacement: "Flag.optional" note: "Use the moved combinator; it still returns Option." @@ -92,7 +92,7 @@ replacement: "Flag.Flag" note: "Options was renamed to Flag in effect/unstable/cli." "@effect/cli/Options#Options.BooleanOptionsConfig": - replacement: "Flag.boolean + Flag.withAlias + Flag.map" + replacement: "Flag.Boolean + Flag.withAlias + Flag.map" note: "The config object was removed; aliases and value inversion are combinators, while custom negation names need application logic." "@effect/cli/Options#Options.PathOptionsConfig": replacement: "{ readonly mustExist?: boolean }" @@ -116,17 +116,17 @@ replacement: "Command.runWith" note: "Raw argv processing is now whole-command execution; no public standalone flag tokenizer remains." "@effect/cli/Options#redacted": - replacement: "Flag.redacted" + replacement: "Flag.Redacted" note: "Use the moved constructor." "@effect/cli/Options#repeated": replacement: "Flag.variadic" note: "Renamed to variadic; pass optional min and max bounds." "@effect/cli/Options#secret": - replacement: "Flag.redacted" + replacement: "Flag.Redacted" note: "The deprecated Secret constructor was removed; use Redacted-backed input." "@effect/cli/Options#text": - replacement: "Flag.string" - note: "Renamed from text to string." + replacement: "Flag.String" + note: "Renamed from text to String." "@effect/cli/Options#withAlias": replacement: "Flag.withAlias" note: "Use the moved combinator." diff --git a/repos/effect/migration/annotations/effect__cli__Primitive.yaml b/repos/effect/migration/annotations/effect__cli__Primitive.yaml index 35082dfc96..ab6eec5193 100644 --- a/repos/effect/migration/annotations/effect__cli__Primitive.yaml +++ b/repos/effect/migration/annotations/effect__cli__Primitive.yaml @@ -1,15 +1,15 @@ "@effect/cli/Primitive#boolean": - replacement: "Primitive.boolean" - note: "Boolean is now a singleton value; defaults belong on Flag.boolean or withDefault." + replacement: "Primitive.Boolean" + note: "Boolean is now a singleton value; defaults belong on Flag.Boolean or withDefault." "@effect/cli/Primitive#choice": - replacement: "Primitive.choice" + replacement: "Primitive.Choice" note: "Use the moved constructor." "@effect/cli/Primitive#date": - replacement: "Primitive.date" + replacement: "Primitive.Date" note: "Date is now a singleton Primitive value." "@effect/cli/Primitive#float": - replacement: "Primitive.float" - note: "Float is now a singleton Primitive value and rejects non-finite numbers." + replacement: "Primitive.Finite" + note: "Finite is now a singleton Primitive value and rejects non-finite numbers." "@effect/cli/Primitive#getChoices": replacement: "none" note: "Choice introspection is internal in v4; retain alternatives in application code when needed." @@ -17,8 +17,8 @@ replacement: "none" note: "Primitive-level help generation was removed from the public API." "@effect/cli/Primitive#integer": - replacement: "Primitive.integer" - note: "Integer is now a singleton Primitive value." + replacement: "Primitive.Int" + note: "Int is now a singleton Primitive value." "@effect/cli/Primitive#isBool": replacement: "none" note: "The boolean Primitive predicate is internal in v4." @@ -35,8 +35,8 @@ replacement: "none" note: "The public Primitive type-id symbol was removed." "@effect/cli/Primitive#text": - replacement: "Primitive.string" - note: "Renamed from text to string." + replacement: "Primitive.String" + note: "Renamed from text to String." "@effect/cli/Primitive#validate": replacement: "primitive.parse(value)" note: "Parsing is now the Primitive.parse method over a string; defaults and case normalization moved out of this layer." diff --git a/repos/effect/migration/annotations/effect__cli__Prompt.yaml b/repos/effect/migration/annotations/effect__cli__Prompt.yaml index 8c1f6ffb17..04071fb7be 100644 --- a/repos/effect/migration/annotations/effect__cli__Prompt.yaml +++ b/repos/effect/migration/annotations/effect__cli__Prompt.yaml @@ -4,24 +4,42 @@ "@effect/cli/Prompt#All.Return": replacement: "Prompt.All.Return" note: "The collection result helper remains under Prompt.All." +"@effect/cli/Prompt#confirm": + replacement: "Prompt.Confirm" + note: "Use the renamed constructor." +"@effect/cli/Prompt#custom": + replacement: "Prompt.Custom" + note: "Use the renamed constructor; both overloads are preserved." "@effect/cli/Prompt#date": - replacement: "Prompt.date" + replacement: "Prompt.Date" note: "Use the moved constructor." "@effect/cli/Prompt#file": - replacement: "Prompt.file" + replacement: "Prompt.File" note: "Use the moved constructor; v4 also supports a default selected path." "@effect/cli/Prompt#flatMap": replacement: "Prompt.flatMap" note: "Use the moved combinator." "@effect/cli/Prompt#float": - replacement: "Prompt.float" + replacement: "Prompt.Number" note: "Use the moved constructor; v4 also supports a default value." +"@effect/cli/Prompt#hidden": + replacement: "Prompt.Hidden" + note: "Use the renamed constructor." "@effect/cli/Prompt#integer": - replacement: "Prompt.integer" + replacement: "Prompt.Int" note: "Use the moved constructor; v4 also supports a default value." +"@effect/cli/Prompt#list": + replacement: "Prompt.List" + note: "Use the renamed constructor." "@effect/cli/Prompt#map": replacement: "Prompt.map" note: "Use the moved combinator." +"@effect/cli/Prompt#multiSelect": + replacement: "Prompt.MultiSelect" + note: "Use the renamed constructor." +"@effect/cli/Prompt#password": + replacement: "Prompt.Password" + note: "Use the renamed constructor." "@effect/cli/Prompt#Prompt": replacement: "Prompt.Prompt" note: "The model moved to effect/unstable/cli; quitting now fails with Terminal.QuitError." @@ -34,6 +52,18 @@ "@effect/cli/Prompt#PromptTypeId": replacement: "Prompt.isPrompt" note: "The public type-id symbol was removed; use the runtime guard." +"@effect/cli/Prompt#select": + replacement: "Prompt.Select" + note: "Use the renamed constructor." "@effect/cli/Prompt#text": - replacement: "Prompt.text" + replacement: "Prompt.String" note: "Use the moved constructor." +"@effect/cli/Prompt#toggle": + replacement: "Prompt.Toggle" + note: "Use the renamed constructor." +"@effect/cli/Prompt#Prompt.IntegerOptions": + replacement: "Prompt.IntOptions" + note: "Use the renamed public options type for Prompt.Int; option fields are preserved." +"@effect/cli/Prompt#Prompt.FloatOptions": + replacement: "Prompt.NumberOptions" + note: "Use the renamed public options type for Prompt.Number; it still extends the integer options type, now IntOptions." diff --git a/repos/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml b/repos/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml index 6edba37157..57ef342ba0 100644 --- a/repos/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml +++ b/repos/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml @@ -1,9 +1,9 @@ "@effect/cluster/EntityProxyServer#layerHttpApi": replacement: "effect/unstable/cluster/EntityProxyServer#layerHttpApi" - note: "Moved into core Effect. Use v4 HttpApi identifiers and Rpc.ServicesServer requirements." + note: "Moved into core Effect. Use v4 HttpApi identifiers and provide Sharding plus both Rpc.ServicesServer and Rpc.ServicesClient codec requirements." "@effect/cluster/EntityProxyServer#layerRpcHandlers": replacement: "effect/unstable/cluster/EntityProxyServer#layerRpcHandlers" - note: "Moved into core Effect; the service requirement is now Rpc.ServicesServer rather than Rpc.Context." + note: "Moved into core Effect; replace Rpc.Context with both Rpc.ServicesServer and Rpc.ServicesClient codec requirements, alongside Sharding." "@effect/cluster/EntityProxyServer#RpcHandlers": replacement: "effect/unstable/cluster/EntityProxyServer#RpcHandlers" note: "Moved into core Effect and updated for the additional v4 Rpc requirements type parameter." diff --git a/repos/effect/migration/annotations/effect__cluster__Reply.yaml b/repos/effect/migration/annotations/effect__cluster__Reply.yaml index aecd2243b8..842dd729a6 100644 --- a/repos/effect/migration/annotations/effect__cluster__Reply.yaml +++ b/repos/effect/migration/annotations/effect__cluster__Reply.yaml @@ -7,3 +7,6 @@ "@effect/cluster/Reply#TypeId": replacement: "none" note: "The reply marker is private in v4. Use Reply.isReply for runtime refinement." +"@effect/cluster/Reply#Reply": + replacement: "Reply.Reply(rpc, codecFor)" + note: "Pass the transport codec. Decoding replies requires Rpc.ServicesClient; encoding replies requires Rpc.ServicesServer." diff --git a/repos/effect/migration/annotations/effect__experimental__EventLogRemote.yaml b/repos/effect/migration/annotations/effect__experimental__EventLogRemote.yaml index 281f01ae7e..c0016a6dcd 100644 --- a/repos/effect/migration/annotations/effect__experimental__EventLogRemote.yaml +++ b/repos/effect/migration/annotations/effect__experimental__EventLogRemote.yaml @@ -6,16 +6,16 @@ note: ChangesRpc replaces the separate request and response models with one streaming RPC. "@effect/experimental/EventLogRemote#decodeRequest": replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs - note: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific request decoder. + note: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific request decoder. "@effect/experimental/EventLogRemote#decodeResponse": replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs - note: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific response decoder. + note: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific response decoder. "@effect/experimental/EventLogRemote#encodeRequest": replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs - note: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific request encoder. + note: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific request encoder. "@effect/experimental/EventLogRemote#encodeResponse": replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs - note: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific response encoder. + note: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific response encoder. "@effect/experimental/EventLogRemote#EventLogRemote": replacement: effect/unstable/eventlog/EventLogRemote#EventLogRemote note: Use the v4 Context.Service; methods now take storeId-aware options. @@ -27,7 +27,7 @@ note: HelloResponse replaces Hello and includes the v4 authentication challenge; HelloRpc defines the endpoint. "@effect/experimental/EventLogRemote#layerWebSocket": replacement: effect/unstable/eventlog/EventLogRemote#layerEncrypted + effect/unstable/rpc/RpcClient#layerProtocolSocket - note: Compose the encrypted remote with the generic socket protocol, MsgPack serialization, and a Socket provider. + note: Compose the encrypted remote with the generic socket protocol, SchemaBinary serialization, and a Socket provider. "@effect/experimental/EventLogRemote#layerWebSocketBrowser": replacement: effect/unstable/eventlog/EventLogRemote#layerEncrypted + effect/unstable/rpc/RpcClient#layerProtocolSocket + @effect/platform-browser/BrowserSocket#layerWebSocket note: Compose the encrypted remote and generic RPC socket protocol with the browser WebSocket layer. @@ -38,14 +38,14 @@ replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs note: EventLogRemoteRpcs and generic RPC serialization replace the old protocol request union. "@effect/experimental/EventLogRemote#ProtocolRequestMsgPack": - replacement: effect/unstable/rpc/RpcSerialization#layerMsgPack - note: Use the generic MsgPack RPC serialization layer instead of a request-specific schema. + replacement: effect/unstable/rpc/RpcSerialization#layerSchemaBinary + note: Use the generic SchemaBinary RPC serialization layer instead of a request-specific schema. "@effect/experimental/EventLogRemote#ProtocolResponse": replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs note: EventLogRemoteRpcs and generic RPC serialization replace the old protocol response union. "@effect/experimental/EventLogRemote#ProtocolResponseMsgPack": - replacement: effect/unstable/rpc/RpcSerialization#layerMsgPack - note: Use the generic MsgPack RPC serialization layer instead of a response-specific schema. + replacement: effect/unstable/rpc/RpcSerialization#layerSchemaBinary + note: Use the generic SchemaBinary RPC serialization layer instead of a response-specific schema. "@effect/experimental/EventLogRemote#RemoteAdditions": replacement: none note: This unused protocol model has no v4 counterpart. diff --git a/repos/effect/migration/annotations/effect__experimental__PersistedQueue.yaml b/repos/effect/migration/annotations/effect__experimental__PersistedQueue.yaml index bd53323f6e..68aada2327 100644 --- a/repos/effect/migration/annotations/effect__experimental__PersistedQueue.yaml +++ b/repos/effect/migration/annotations/effect__experimental__PersistedQueue.yaml @@ -7,6 +7,9 @@ "@effect/experimental/PersistedQueue#make": replacement: effect/unstable/persistence/PersistedQueue#make note: Import make from the v4 unstable PersistedQueue module. +"@effect/experimental/PersistedQueue#ErrorTypeId": + replacement: effect/unstable/persistence/PersistedQueue#ErrorTypeId + note: Retained as a string brand; the runtime marker now uses the persistence module path. "@effect/experimental/PersistedQueue#TypeId": replacement: effect/unstable/persistence/PersistedQueue#TypeId note: Import TypeId from the v4 unstable PersistedQueue module; it is now a string brand. diff --git a/repos/effect/migration/annotations/effect__experimental__RateLimiter.yaml b/repos/effect/migration/annotations/effect__experimental__RateLimiter.yaml index 4ca45b2d72..32537e7580 100644 --- a/repos/effect/migration/annotations/effect__experimental__RateLimiter.yaml +++ b/repos/effect/migration/annotations/effect__experimental__RateLimiter.yaml @@ -10,6 +10,12 @@ "@effect/experimental/RateLimiter#RateLimiterError": replacement: effect/unstable/persistence/RateLimiter#RateLimiterError note: The retained name is now a wrapper error class whose reason is RateLimitExceeded or RateLimitStoreError. +"@effect/experimental/RateLimiter#ErrorTypeId": + replacement: effect/unstable/persistence/RateLimiter#ErrorTypeId + note: Retained as a string brand; the runtime marker now uses the persistence module path. "@effect/experimental/RateLimiter#TypeId": replacement: effect/unstable/persistence/RateLimiter#TypeId note: Import TypeId from the v4 unstable RateLimiter module; it is now a string brand. +"@effect/experimental/RateLimiter#RateLimiterStore": + replacement: RateLimiter.RateLimiterStore + note: "Use the Context.Service class from effect/unstable/persistence/RateLimiter. Custom tokenBucket implementations must return [remaining, elapsedMillis] instead of a number, preserving fractional counts and elapsed refill time." diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunStream.yaml b/repos/effect/migration/annotations/effect__platform-bun__BunStream.yaml index 4b343a35c3..dee586b782 100644 --- a/repos/effect/migration/annotations/effect__platform-bun__BunStream.yaml +++ b/repos/effect/migration/annotations/effect__platform-bun__BunStream.yaml @@ -1,6 +1,6 @@ "@effect/platform-bun/BunStream#FromReadableOptions": replacement: "Pick[0], \"chunkSize\" | \"closeOnDone\">" - note: "The named interface was inlined into the constructor options; chunkSize is now a number and the full options also contain evaluate, onError, and bufferSize." + note: "The named interface was inlined into the constructor options; chunkSize is now a number and the full options also contain evaluate and onError. The ignored bufferSize option was removed." "@effect/platform-bun/BunStream#FromWritableOptions": replacement: "Pick[0], \"endOnDone\" | \"encoding\">" note: "The named interface was inlined into BunSink.fromWritable and duplex constructor options." diff --git a/repos/effect/migration/annotations/effect__platform-node__index.yaml b/repos/effect/migration/annotations/effect__platform-node__index.yaml index d2e0c66e95..3e30ad1652 100644 --- a/repos/effect/migration/annotations/effect__platform-node__index.yaml +++ b/repos/effect/migration/annotations/effect__platform-node__index.yaml @@ -1,3 +1,3 @@ "@effect/platform-node/index": replacement: "@effect/platform-node" - note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/platform-node package root or import specific modules directly." + note: "The explicit /index entrypoint was removed; import Node-prefixed namespaces from the @effect/platform-node package root or import specific modules directly. Undici is no longer in the root barrel; import it from @effect/platform-node/Undici or directly from undici." diff --git a/repos/effect/migration/annotations/effect__platform__Cookies.yaml b/repos/effect/migration/annotations/effect__platform__Cookies.yaml index 0cf3cfa236..ef4edc1e9f 100644 --- a/repos/effect/migration/annotations/effect__platform__Cookies.yaml +++ b/repos/effect/migration/annotations/effect__platform__Cookies.yaml @@ -19,3 +19,6 @@ "@effect/platform/Cookies#unsafeSetAll": replacement: "Cookies.setAllUnsafe" note: "Renamed to put Unsafe last; the dual all-or-throw behavior is retained." +"@effect/platform/Cookies#CookiesError": + replacement: "Cookies.CookiesError" + note: "The error tag is CookiesError rather than CookieError. Update catchTag calls and _tag comparisons; validation details are in the reason field." diff --git a/repos/effect/migration/annotations/effect__platform__FileSystem.yaml b/repos/effect/migration/annotations/effect__platform__FileSystem.yaml index ddd7a88d31..7dafaa3727 100644 --- a/repos/effect/migration/annotations/effect__platform__FileSystem.yaml +++ b/repos/effect/migration/annotations/effect__platform__FileSystem.yaml @@ -13,9 +13,15 @@ "@effect/platform/FileSystem#FileTypeId": replacement: "typeof FileSystem.FileTypeId" note: "The runtime marker remains exported, but the separate type alias was removed." +"@effect/platform/FileSystem#GiB": + replacement: "ByteSize.gibibytes" + note: "Use the ByteSize binary unit constructor." "@effect/platform/FileSystem#isFile": replacement: "FileSystem.isFile" note: "The guard remains after moving the module to effect/FileSystem." +"@effect/platform/FileSystem#KiB": + replacement: "ByteSize.kibibytes" + note: "Use the ByteSize binary unit constructor." "@effect/platform/FileSystem#layerNoop": replacement: "FileSystem.layerNoop" note: "The helper remains after moving the module to effect/FileSystem." @@ -34,9 +40,15 @@ "@effect/platform/FileSystem#MakeTempFileOptions": replacement: "NonNullable[0]>" note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#MiB": + replacement: "ByteSize.mebibytes" + note: "Use the ByteSize binary unit constructor." "@effect/platform/FileSystem#OpenFileOptions": replacement: "NonNullable[1]>" note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#PiB": + replacement: "ByteSize.pebibytes" + note: "Use the ByteSize binary unit constructor." "@effect/platform/FileSystem#ReadDirectoryOptions": replacement: "NonNullable[1]>" note: "Operation option interfaces are inline in the v4 FileSystem service." @@ -47,11 +59,17 @@ replacement: "NonNullable[1]>" note: "Operation option interfaces are inline in the v4 FileSystem service." "@effect/platform/FileSystem#Size": - replacement: "FileSystem.Size" - note: "The branded bigint size type remains after moving the module to effect/FileSystem." + replacement: "ByteSize.ByteSize" + note: "Use ByteSize.bytes or unit constructors for file sizes. Truncation lengths, buffer sizes, and read/write counts use number. File.seek takes and returns signed bigint positions; it can fail with PlatformError, including BadArgument when seeking before the start." +"@effect/platform/FileSystem#SizeInput": + replacement: "ByteSize.Input" + note: "File-size and path-backed range inputs use ByteSize.Input. Truncation lengths, Web File ranges, and buffer sizes use number." "@effect/platform/FileSystem#StreamOptions": replacement: "NonNullable[1]>" - note: "Stream options are inline; bufferSize was removed while bytesToRead, chunkSize, and offset remain." + note: "Stream options are inline; bufferSize was removed, bytesToRead and offset accept ByteSize inputs, and chunkSize uses number." +"@effect/platform/FileSystem#TiB": + replacement: "ByteSize.tebibytes" + note: "Use the ByteSize binary unit constructor." "@effect/platform/FileSystem#WatchEventCreate": replacement: "FileSystem.WatchEvent.Create" note: "The constructor was removed; construct a tagged object with _tag: \"Create\" and path." diff --git a/repos/effect/migration/annotations/effect__platform__Headers.yaml b/repos/effect/migration/annotations/effect__platform__Headers.yaml index ef95fbf84f..2cd2ffa57a 100644 --- a/repos/effect/migration/annotations/effect__platform__Headers.yaml +++ b/repos/effect/migration/annotations/effect__platform__Headers.yaml @@ -29,11 +29,11 @@ replacement: "Headers.remove / Headers.removeMany" note: "Use remove for one name or removeMany for an iterable; RegExp removal requires enumerating matching names." "@effect/platform/Headers#schema": - replacement: "Headers.HeadersSchema" - note: "The encoded-record and self schemas were consolidated into HeadersSchema." + replacement: "Schema.Headers" + note: "The encoded-record and self schemas were consolidated and moved to effect/Schema as Schema.Headers." "@effect/platform/Headers#schemaFromSelf": - replacement: "Headers.HeadersSchema" - note: "The encoded-record and self schemas were consolidated into HeadersSchema." + replacement: "Schema.Headers" + note: "The encoded-record and self schemas were consolidated and moved to effect/Schema as Schema.Headers." "@effect/platform/Headers#set": replacement: "Headers.set" note: "Retained with the same dual signature and lowercase key normalization." diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml b/repos/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml index 3742d27e57..35231d5870 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml @@ -8,8 +8,8 @@ replacement: "effect/unstable/httpapi/HttpApiBuilder#group" note: "The group layer remains; names are now identifiers and API/group global error channels are gone." "@effect/platform/HttpApiBuilder#handler": - replacement: "effect/unstable/httpapi/HttpApiBuilder#endpoint" - note: "Use endpoint for a standalone typed endpoint implementation; inside a group pass callbacks to handlers.handle." + replacement: "effect/unstable/httpapi/HttpApiBuilder#handler" + note: "The typed callback helper remains; names are now identifiers and API/group global error channels are gone. Pass the returned callback to handlers.handle." "@effect/platform/HttpApiBuilder#Handlers": replacement: "effect/unstable/httpapi/HttpApiBuilder#Handlers" note: "Handlers now tracks an endpoint map and handled identifiers. Prefer Handlers.FromGroup." @@ -25,6 +25,9 @@ "@effect/platform/HttpApiBuilder#HandlersTypeId": replacement: "none" note: "The exported symbol was removed; do not inspect or construct the private Handlers marker." +"@effect/platform/HttpApiBuilder#Middleware": + replacement: "none" + note: "The API-specific middleware service tag was removed. Declared HttpApiMiddleware services are applied while routes are built; use HttpRouter.middleware for additional global middleware." "@effect/platform/HttpApiBuilder#httpApp": replacement: "effect/unstable/http/HttpRouter#toHttpEffect" note: "Build the application from the assembled API route layer; HTTP apps are Effects in v4." diff --git a/repos/effect/migration/annotations/effect__platform__HttpBody.yaml b/repos/effect/migration/annotations/effect__platform__HttpBody.yaml index 4e8e46f1b1..8258bcc0cf 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpBody.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpBody.yaml @@ -12,10 +12,10 @@ note: "The error type-id is private in v4; identify the exported error class instead." "@effect/platform/HttpBody#file": replacement: "HttpBody.file" - note: "Retained; bufferSize was replaced by chunkSize and the other file options remain." + note: "Retained; bufferSize was replaced by numeric chunkSize. Offset and bytesToRead accept ByteSize.Input. Invalid ranges and a final EOF-clamped content length above Number.MAX_SAFE_INTEGER fail with PlatformError / BadArgument." "@effect/platform/HttpBody#fileInfo": replacement: "HttpBody.fileFromInfo" - note: "Renamed; it still uses supplied File.Info for content length and requires FileSystem." + note: "Renamed; it uses supplied File.Info with ByteSize size metadata and requires FileSystem. Offset and bytesToRead accept ByteSize.Input, while chunkSize is numeric. Invalid ranges and a final EOF-clamped content length above Number.MAX_SAFE_INTEGER fail with PlatformError / BadArgument." "@effect/platform/HttpBody#formData": replacement: "HttpBody.formData" note: "Retained with the same Web FormData input." diff --git a/repos/effect/migration/annotations/effect__platform__HttpClientResponse.yaml b/repos/effect/migration/annotations/effect__platform__HttpClientResponse.yaml index cc48c14c85..49154affcb 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpClientResponse.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpClientResponse.yaml @@ -25,3 +25,6 @@ "@effect/platform/HttpClientResponse#TypeId": replacement: "typeof HttpClientResponse.TypeId" note: "TypeId remains public but is now a string constant; use typeof in type position." +"@effect/platform/HttpClientResponse#HttpClientResponse": + replacement: "HttpClientResponse.HttpClientResponse" + note: "Custom implementations must supply the required url string. It represents the resolved URL including query parameters and excluding the hash, uses the final URL after redirects, and is empty when unknown." diff --git a/repos/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml b/repos/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml index f720fb5d18..055745ce53 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml @@ -1,9 +1,9 @@ "@effect/platform/HttpIncomingMessage#MaxBodySize": replacement: "HttpIncomingMessage.MaxBodySize" - note: "Changed from a Reference subclass holding Option to Context.Reference." + note: "Changed from a Reference subclass holding Option to Context.Reference." "@effect/platform/HttpIncomingMessage#TypeId": replacement: "typeof HttpIncomingMessage.TypeId" note: "TypeId remains public but is now a string constant; use typeof in type position." "@effect/platform/HttpIncomingMessage#withMaxBodySize": replacement: "Effect.provideService(HttpIncomingMessage.MaxBodySize, size)" - note: "The helper was removed; provide FileSystem.Size(input) or undefined directly." + note: "The helper was removed; provide a ByteSize value or undefined directly." diff --git a/repos/effect/migration/annotations/effect__platform__HttpPlatform.yaml b/repos/effect/migration/annotations/effect__platform__HttpPlatform.yaml index ba4d673800..523d462f03 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpPlatform.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpPlatform.yaml @@ -1,12 +1,12 @@ "@effect/platform/HttpPlatform#HttpPlatform": replacement: "HttpPlatform.HttpPlatform" - note: "The service is now a Context.Service class; use its Service member for the implementation type." + note: "The service is now a Context.Service class; use its Service member for the implementation type. Path-backed offset and bytesToRead accept ByteSize.Input, while chunkSize and all Web File range options use number." "@effect/platform/HttpPlatform#layer": replacement: "HttpPlatform.layer" note: "Retained as the default file-response layer." "@effect/platform/HttpPlatform#make": replacement: "HttpPlatform.make" - note: "Retained; v4 returns the service implementation and uses updated file stream options." + note: "Retained; v4 returns the service implementation. The fileResponse callback receives contentLength as bigint, while start and end remain numbers. Web File range options use number." "@effect/platform/HttpPlatform#TypeId": replacement: "none" note: "The public type id was removed; use the HttpPlatform Context.Service class." diff --git a/repos/effect/migration/annotations/effect__platform__HttpServer.yaml b/repos/effect/migration/annotations/effect__platform__HttpServer.yaml index be98b6d5d6..c49498a3fc 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpServer.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpServer.yaml @@ -1,3 +1,6 @@ +"@effect/platform/HttpServer#Address": + replacement: "effect/unstable/net/NetAddress#SocketAddress" + note: "Replaced by the shared concrete internet-or-Unix socket address union." "@effect/platform/HttpServer#addressWith": replacement: "HttpServer.HttpServer.use(({ address }) => effect(address))" note: "The accessor was removed; read the service and pass its Address to the callback." @@ -14,14 +17,14 @@ replacement: "none" note: "The unused respond option model was removed with no shared v4 counterpart." "@effect/platform/HttpServer#TcpAddress": - replacement: "HttpServer.TcpAddress" - note: "Moved unchanged." + replacement: "effect/unstable/net/NetAddress#InetAddress" + note: "Replaced by the shared resolved internet-address model; use address and port instead of hostname and port." "@effect/platform/HttpServer#TypeId": replacement: "none" note: "The public TypeId was removed; HttpServer is now a Context.Service class." "@effect/platform/HttpServer#UnixAddress": - replacement: "HttpServer.UnixAddress" - note: "Moved unchanged." + replacement: "effect/unstable/net/NetAddress#UnixPathAddress" + note: "Replaced by the shared Unix filesystem-path address model." "@effect/platform/HttpServer#serve": replacement: "effect/unstable/http/HttpServer#serve" note: "Moved to the v4 HTTP module; the application is now an Effect producing HttpServerResponse rather than the separate HttpApp model." diff --git a/repos/effect/migration/annotations/effect__platform__HttpServerResponse.yaml b/repos/effect/migration/annotations/effect__platform__HttpServerResponse.yaml index 86d7dfd144..e91e0818b2 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpServerResponse.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpServerResponse.yaml @@ -6,7 +6,7 @@ note: "Now effectful and safe; use expireCookieUnsafe for synchronous throwing behavior." "@effect/platform/HttpServerResponse#file": replacement: "HttpServerResponse.file" - note: "Retained with updated FileSystem stream options." + note: "Retained; offset and bytesToRead accept ByteSize.Input, while chunkSize uses number. Path-backed responses validate ranges and clamp content length to the available bytes." "@effect/platform/HttpServerResponse#formData": replacement: "HttpServerResponse.formData" note: "Moved unchanged." @@ -61,3 +61,6 @@ "@effect/platform/HttpServerResponse#urlParams": replacement: "HttpServerResponse.urlParams" note: "Retained and widened to accept UrlParams.Input." +"@effect/platform/HttpServerResponse#fileWeb": + replacement: "HttpServerResponse.fileWeb" + note: "Web File offset, bytesToRead, and chunkSize options use number, unlike path-backed ByteSize.Input ranges." diff --git a/repos/effect/migration/annotations/effect__platform__MsgPack.yaml b/repos/effect/migration/annotations/effect__platform__MsgPack.yaml index 3c4d47b6bf..e6ff8e67ce 100644 --- a/repos/effect/migration/annotations/effect__platform__MsgPack.yaml +++ b/repos/effect/migration/annotations/effect__platform__MsgPack.yaml @@ -1,24 +1,30 @@ +"@effect/platform/MsgPack": + replacement: "effect/unstable/encoding/SchemaBinary" + note: "MessagePack support was removed. Schema-aware encode, decode, and duplex now live on SchemaBinary. Untyped MessagePack of unknown values has no replacement." "@effect/platform/MsgPack#duplex": - replacement: "Msgpack.duplex" - note: "The API moved to effect/unstable/encoding/Msgpack." + replacement: "SchemaBinary.duplex" + note: "MessagePack was removed. Wrap a byte channel with SchemaBinary.duplex and explicit input and output schemas." "@effect/platform/MsgPack#duplexSchema": - replacement: "Msgpack.duplexSchema" - note: "The API moved to effect/unstable/encoding/Msgpack and uses v4 Schema constraints." + replacement: "SchemaBinary.duplex" + note: "MessagePack was removed. Use SchemaBinary.duplex with the v4 Schema model." "@effect/platform/MsgPack#ErrorTypeId": - replacement: "Msgpack.MsgPackError" - note: "The public error type-id alias was removed; use the MsgPackError class." + replacement: "Schema.SchemaError" + note: "MsgPackError was removed with MessagePack. SchemaBinary channels fail with Schema.SchemaError." +"@effect/platform/MsgPack#MsgPackError": + replacement: "Schema.SchemaError" + note: "MsgPackError was removed with MessagePack. SchemaBinary channels fail with Schema.SchemaError." "@effect/platform/MsgPack#pack": - replacement: "Msgpack.encode" - note: "The MessagePack channel constructor was renamed from pack to encode." + replacement: "SchemaBinary.encode" + note: "MessagePack was removed. Encode schema values to binary frames with SchemaBinary.encode." "@effect/platform/MsgPack#packSchema": - replacement: "Msgpack.encodeSchema" - note: "The schema-aware pack channel was renamed to encodeSchema." + replacement: "SchemaBinary.encode" + note: "MessagePack was removed. Use SchemaBinary.encode with the v4 Schema model." "@effect/platform/MsgPack#schema": - replacement: "Msgpack.schema" - note: "The schema helper remains in the moved module and uses the v4 Schema model." + replacement: "SchemaBinary.toCodec" + note: "MessagePack was removed. Derive a binary codec from a Schema with SchemaBinary.toCodec." "@effect/platform/MsgPack#unpack": - replacement: "Msgpack.decode" - note: "The MessagePack channel constructor was renamed from unpack to decode." + replacement: "SchemaBinary.decode" + note: "MessagePack was removed. Decode binary frames with SchemaBinary.decode." "@effect/platform/MsgPack#unpackSchema": - replacement: "Msgpack.decodeSchema" - note: "The schema-aware unpack channel was renamed to decodeSchema." + replacement: "SchemaBinary.decode" + note: "MessagePack was removed. Use SchemaBinary.decode with the v4 Schema model." diff --git a/repos/effect/migration/annotations/effect__platform__Multipart.yaml b/repos/effect/migration/annotations/effect__platform__Multipart.yaml index e5f43102ea..3e1a863458 100644 --- a/repos/effect/migration/annotations/effect__platform__Multipart.yaml +++ b/repos/effect/migration/annotations/effect__platform__Multipart.yaml @@ -18,10 +18,10 @@ note: "The guard remains in effect/unstable/http/Multipart." "@effect/platform/Multipart#MaxFieldSize": replacement: "Multipart.MaxFieldSize" - note: "The setting remains but is now a Context.Reference." + note: "Now a Context.Reference; provide a value such as ByteSize.bytes(100)." "@effect/platform/Multipart#MaxFileSize": replacement: "Multipart.MaxFileSize" - note: "The setting remains as a Context.Reference; use undefined rather than Option.none for no limit." + note: "Now a Context.Reference; provide ByteSize.bytes(100), for example, or undefined for no limit." "@effect/platform/Multipart#MaxParts": replacement: "Multipart.MaxParts" note: "The setting remains as a Context.Reference; use undefined rather than Option.none for no limit." @@ -42,10 +42,10 @@ note: "Build the multipart limit context and provide it to the stream; Option-valued limits became optional plain values." "@effect/platform/Multipart#withMaxFieldSize": replacement: "Effect.provideService(Multipart.MaxFieldSize, size)" - note: "Provide the v4 Context.Reference around the effect." + note: "Provide a ByteSize value, such as ByteSize.bytes(100). To normalize ByteSize.Input options, use Multipart.limitsServices." "@effect/platform/Multipart#withMaxFileSize": replacement: "Effect.provideService(Multipart.MaxFileSize, size)" - note: "Provide the v4 Context.Reference around the effect, converting Option.none to undefined." + note: "Replace Option.none with undefined and Option.some(value) with a normalized ByteSize value. To normalize ByteSize.Input options, use Multipart.limitsServices." "@effect/platform/Multipart#withMaxParts": replacement: "Effect.provideService(Multipart.MaxParts, count)" note: "Provide the v4 Context.Reference around the effect, converting Option.none to undefined." @@ -54,4 +54,4 @@ note: "The channel constructor moved and no longer accepts bufferSize; input and output chunks use non-empty readonly arrays." "@effect/platform/Multipart#withLimits.Options": replacement: "Multipart.withLimits.Options" - note: "Limit fields now use optional plain numbers or SizeInput values; convert Option.none to undefined and Option.some(value) to value." + note: "Limit fields now use optional plain numbers or ByteSize inputs; convert Option.none to undefined and Option.some(value) to value." diff --git a/repos/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml b/repos/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml index 683b937f3f..0bb6527bc2 100644 --- a/repos/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml +++ b/repos/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml @@ -10,6 +10,9 @@ "@effect/platform/OpenApiJsonSchema#Array": replacement: "effect/JsonSchema#JsonSchema" note: "The narrow node interfaces were consolidated into the general object model." +"@effect/platform/OpenApiJsonSchema#Boolean": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow boolean interface was consolidated into the open, dialect-neutral JSON Schema object model." "@effect/platform/OpenApiJsonSchema#Empty": replacement: "effect/JsonSchema#JsonSchema" note: "The narrow node interfaces and special id shapes were removed." @@ -31,6 +34,12 @@ "@effect/platform/OpenApiJsonSchema#makeWithDefs": replacement: "effect/SchemaRepresentation#toJsonSchemaMultiDocument + effect/JsonSchema#toMultiDocumentOpenApi3_1" note: "Definitions are returned separately; build a multi-document representation and convert it to OpenAPI 3.1." +"@effect/platform/OpenApiJsonSchema#Never": + replacement: "effect/JsonSchema#JsonSchema" + note: "The special never-schema interface was consolidated into the open JSON Schema object model; represent it with a not constraint." +"@effect/platform/OpenApiJsonSchema#Number": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow number interface was consolidated into the open, dialect-neutral JSON Schema object model." "@effect/platform/OpenApiJsonSchema#Numeric": replacement: "effect/JsonSchema#JsonSchema" note: "The narrow numeric interfaces were consolidated into the general object model." @@ -43,3 +52,9 @@ "@effect/platform/OpenApiJsonSchema#Root": replacement: "effect/JsonSchema#MultiDocument" note: "OpenAPI generation keeps roots in schemas and shared components in definitions; the inline-definitions root model is gone." +"@effect/platform/OpenApiJsonSchema#String": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow string interface was consolidated into the open, dialect-neutral JSON Schema object model." +"@effect/platform/OpenApiJsonSchema#Void": + replacement: "effect/JsonSchema#JsonSchema" + note: "The special void-schema interface was consolidated into the open JSON Schema object model." diff --git a/repos/effect/migration/annotations/effect__platform__Socket.yaml b/repos/effect/migration/annotations/effect__platform__Socket.yaml index cb71a5141c..ecfcfec2da 100644 --- a/repos/effect/migration/annotations/effect__platform__Socket.yaml +++ b/repos/effect/migration/annotations/effect__platform__Socket.yaml @@ -2,8 +2,8 @@ replacement: "Socket.CloseEvent" note: "The close-event marker is internal in v4; use the CloseEvent class or Socket.isCloseEvent." "@effect/platform/Socket#currentSendQueueCapacity": - replacement: "Socket.SendQueueCapacity" - note: "The FiberRef was replaced by a defaulted Context.Reference." + replacement: none + note: "The send queue was removed. The v4 Socket is pull-based: acquire socket.reader in a scope and pull frame batches; writes apply the transport's native backpressure." "@effect/platform/Socket#layerWebSocket": replacement: "Socket.layerWebSocket" note: "The constructor remains in effect/unstable/socket/Socket; its URL may now also be an Effect." @@ -25,3 +25,12 @@ "@effect/platform/Socket#WebSocketConstructor": replacement: "Socket.WebSocketConstructor" note: "The service moved to effect/unstable/socket/Socket and is now a Context.Service class." +"@effect/platform/Socket#defaultCloseCodeIsError": + replacement: none + note: "Sockets no longer classify close codes; every close fails the reader's pull with a SocketError wrapping SocketCloseError. Consumers that treat a close as normal catch the error." +"@effect/platform/Socket#fromTransformStream": + replacement: "Socket.fromTransformStream" + note: "The constructor remains in effect/unstable/socket/Socket but drops closeCodeIsError; every close fails the reader's pull with a SocketError wrapping SocketCloseError." +"@effect/platform/Socket#toChannelMap": + replacement: none + note: "The v4 Socket read side is an Effect that never completes via Cause.Done; map frames by acquiring Socket.readerBytes or Socket.readerString, or Effect.map the reader from socket.reader, and use Socket.toChannel or Socket.toChannelString for duplex channels." diff --git a/repos/effect/migration/annotations/effect__platform__SocketServer.yaml b/repos/effect/migration/annotations/effect__platform__SocketServer.yaml index b7a20dbd8c..6367ff8e11 100644 --- a/repos/effect/migration/annotations/effect__platform__SocketServer.yaml +++ b/repos/effect/migration/annotations/effect__platform__SocketServer.yaml @@ -1,9 +1,12 @@ +"@effect/platform/SocketServer#Address": + replacement: "effect/unstable/net/NetAddress#SocketAddress" + note: "Replaced by the shared concrete internet-or-Unix socket address union." "@effect/platform/SocketServer#ErrorTypeId": replacement: "SocketServer.ErrorTypeId" note: "The API moved to effect/unstable/socket/SocketServer and retains this name." "@effect/platform/SocketServer#TcpAddress": - replacement: "SocketServer.TcpAddress" - note: "The API moved to effect/unstable/socket/SocketServer and retains this name." + replacement: "effect/unstable/net/NetAddress#InetAddress" + note: "Replaced by the shared resolved internet-address model; use address and port instead of hostname and port." "@effect/platform/SocketServer#UnixAddress": - replacement: "SocketServer.UnixAddress" - note: "The API moved to effect/unstable/socket/SocketServer and retains this name." + replacement: "effect/unstable/net/NetAddress#UnixPathAddress" + note: "Replaced by the shared Unix filesystem-path address model." diff --git a/repos/effect/migration/annotations/effect__platform__Url.yaml b/repos/effect/migration/annotations/effect__platform__Url.yaml index 951df8c57b..de6c446778 100644 --- a/repos/effect/migration/annotations/effect__platform__Url.yaml +++ b/repos/effect/migration/annotations/effect__platform__Url.yaml @@ -1,3 +1,6 @@ +"@effect/platform/Url#fromString": + replacement: "Url.fromString" + note: "Retained; returns Result with IllegalArgumentError instead of Either with IllegalArgumentException." "@effect/platform/Url#setUrlParams": replacement: "Url.setUrlParams" note: "Retained and widened to accept UrlParams.Input." diff --git a/repos/effect/migration/annotations/effect__platform__UrlParams.yaml b/repos/effect/migration/annotations/effect__platform__UrlParams.yaml index a799f823a1..2e4a856dfe 100644 --- a/repos/effect/migration/annotations/effect__platform__UrlParams.yaml +++ b/repos/effect/migration/annotations/effect__platform__UrlParams.yaml @@ -23,23 +23,23 @@ replacement: "UrlParams.remove" note: "Retained and removes every value for the key." "@effect/platform/UrlParams#schemaFromSelf": - replacement: "UrlParams.UrlParamsSchema" - note: "Renamed to the declaration schema for the v4 wrapper." + replacement: "Schema.UrlParams" + note: "The declaration schema for the v4 wrapper moved to effect/Schema." "@effect/platform/UrlParams#schemaFromString": - replacement: "Schema.String.pipe(Schema.decodeTo(UrlParams.UrlParamsSchema, { decode: SchemaGetter.transform((s) => UrlParams.fromInput(new URLSearchParams(s))), encode: SchemaGetter.transform(UrlParams.toString) }))" + replacement: "Schema.String.pipe(Schema.decodeTo(Schema.UrlParams, { decode: SchemaGetter.transform((s) => UrlParams.fromInput(new URLSearchParams(s))), encode: SchemaGetter.transform(UrlParams.toString) }))" note: "No prebuilt string codec remains; recreate it by transforming between a query string and UrlParams." "@effect/platform/UrlParams#schemaJson": - replacement: "UrlParams.schemaJsonField(field).pipe(Schema.decodeTo(schema), Schema.decodeEffect)" - note: "Compose the field codec with the target schema, then decode it." + replacement: "Schema.JsonFromUrlParamsField(field).pipe(Schema.decodeTo(schema), Schema.decodeEffect)" + note: "The field codec moved to effect/Schema. Compose it with the target schema, then decode it." "@effect/platform/UrlParams#schemaParse": - replacement: "UrlParamsFromString.pipe(Schema.decodeTo(UrlParams.schemaRecord.pipe(Schema.decodeTo(schema))))" + replacement: "UrlParamsFromString.pipe(Schema.decodeTo(Schema.RecordFromUrlParams.pipe(Schema.decodeTo(schema))))" note: "Recreate the removed helper by composing the string, record, and target codecs." "@effect/platform/UrlParams#schemaRecord": - replacement: "UrlParams.schemaRecord.pipe(Schema.decodeTo(schema))" - note: "schemaRecord is now a base codec value; compose it with the target schema." + replacement: "Schema.RecordFromUrlParams.pipe(Schema.decodeTo(schema))" + note: "RecordFromUrlParams is a base codec in effect/Schema; compose it with the target schema." "@effect/platform/UrlParams#schemaStruct": - replacement: "UrlParams.schemaRecord.pipe(Schema.decodeTo(schema), Schema.decodeEffect)" - note: "Compose the record codec with the target schema and decode it." + replacement: "Schema.RecordFromUrlParams.pipe(Schema.decodeTo(schema), Schema.decodeEffect)" + note: "Compose the record codec from effect/Schema with the target schema and decode it." "@effect/platform/UrlParams#set": replacement: "UrlParams.set" note: "Retained and replaces all existing values for the key." @@ -49,3 +49,6 @@ "@effect/platform/UrlParams#toString": replacement: "UrlParams.toString" note: "Retained and broadened to accept any UrlParams.Input." +"@effect/platform/UrlParams#UrlParams": + replacement: "UrlParams.UrlParams" + note: "Import UrlParams from effect/unstable/http. It is now a branded iterable object with a params field rather than a ReadonlyArray; construct it with UrlParams.make or UrlParams.fromInput." diff --git a/repos/effect/migration/annotations/effect__rpc__RpcSerialization.yaml b/repos/effect/migration/annotations/effect__rpc__RpcSerialization.yaml new file mode 100644 index 0000000000..fae2c1f87b --- /dev/null +++ b/repos/effect/migration/annotations/effect__rpc__RpcSerialization.yaml @@ -0,0 +1,15 @@ +"@effect/rpc/RpcSerialization#RpcSerializationError": + replacement: "effect/unstable/rpc/RpcSerialization#MaxBufferSizeExceeded" + note: "Buffer-limit failures now use MaxBufferSizeExceeded. MessagePack-specific decode errors have no counterpart." +"@effect/rpc/RpcSerialization#layerMsgPack": + replacement: "effect/unstable/rpc/RpcSerialization#layerSchemaBinary" + note: "MessagePack RPC serialization was removed. Use SchemaBinary, or layerNdjson when you need newline-delimited JSON framing." +"@effect/rpc/RpcSerialization#layerMsgPackWith": + replacement: "effect/unstable/rpc/RpcSerialization#layerSchemaBinary" + note: "MessagePack RPC serialization was removed. Pass maxFrameSize to layerSchemaBinary; NDJSON buffer limits remain on layerNdjsonWith." +"@effect/rpc/RpcSerialization#makeMsgPack": + replacement: "effect/unstable/rpc/RpcSerialization#layerSchemaBinary" + note: "MessagePack RPC serialization was removed. Construct SchemaBinary serialization with layerSchemaBinary." +"@effect/rpc/RpcSerialization#msgPack": + replacement: "effect/unstable/rpc/RpcSerialization#layerSchemaBinary" + note: "The MessagePack RpcSerialization service value was removed. Provide layerSchemaBinary instead." diff --git a/repos/effect/migration/annotations/effect__sql-pg__PgClient.yaml b/repos/effect/migration/annotations/effect__sql-pg__PgClient.yaml index f74fad15a7..d9ae6977b3 100644 --- a/repos/effect/migration/annotations/effect__sql-pg__PgClient.yaml +++ b/repos/effect/migration/annotations/effect__sql-pg__PgClient.yaml @@ -1,6 +1,9 @@ +"@effect/sql-pg/PgClient#fromPool": + replacement: none + note: "Wrapping an existing node-pg Pool was removed with the native protocol client. Use PgClient.make or PgClient.layer with connection settings." "@effect/sql-pg/PgClient#layerFromPool": - replacement: "PgClient.layerFrom(PgClient.fromPool(options))" - note: "Compose fromPool with layerFrom; layerFrom now accepts an Effect acquiring a PgClient rather than pool options." + replacement: "PgClient.layer" + note: "Wrapping an existing node-pg Pool was removed. Provide connection settings to PgClient.layer instead." "@effect/sql-pg/PgClient#PgClient": replacement: "@effect/sql-pg/PgClient#PgClient" note: "Retained; the service value is now a Context.Service." @@ -8,5 +11,5 @@ replacement: "@effect/sql-pg/PgClient#PgClientConfig / PgPoolConfig" note: "Use PgClientConfig for base settings and PgPoolConfig for make/layer; pool sizing, idle timeout, and connection TTL moved to PgPoolConfig." "@effect/sql-pg/PgClient#PgClientFromPoolOptions": - replacement: "Parameters[0]" - note: "The named type was removed; derive the inline fromPool option type. PgPoolConfig is for creating a managed pool and is not equivalent." + replacement: none + note: "The node-pg Pool wrapper options were removed with fromPool. Use PgClient.PgPoolConfig with PgClient.make or PgClient.layer." diff --git a/repos/effect/migration/annotations/effect__sql__Model.yaml b/repos/effect/migration/annotations/effect__sql__Model.yaml index 00db99efc2..6c82eda852 100644 --- a/repos/effect/migration/annotations/effect__sql__Model.yaml +++ b/repos/effect/migration/annotations/effect__sql__Model.yaml @@ -10,6 +10,9 @@ "@effect/sql/Model#Class": replacement: "effect/unstable/schema/Model#Class" note: "Moved; model variants remain select, insert, update, json, jsonCreate, and jsonUpdate." +"@effect/sql/Model#Date": + replacement: "effect/unstable/schema/Model#Date" + note: "Moved; still serializes DateTime.Utc as a YYYY-MM-DD string." "@effect/sql/Model#DateTimeFromDate": replacement: "effect/Schema#DateTimeUtcFromDate" note: "Moved to core Schema and retains Date to DateTime.Utc conversion." @@ -24,7 +27,7 @@ note: "Renamed and now read-only, with select and json variants only. Use Model.Field with select, update, and json to preserve writable v3 behavior." "@effect/sql/Model#makeDataLoaders": replacement: "effect/unstable/sql/SqlModel#makeResolvers" - note: "Returns RequestResolvers instead of callable loaders; execute with SqlResolver.request and use RequestResolver delay/batch combinators for batching controls." + note: "Returns RequestResolvers instead of callable loaders; execute with SqlResolver.request and use RequestResolver delay/batch combinators for batching controls. The insert resolver requires model decoding services as well as insert-schema encoding services." "@effect/sql/Model#Override": replacement: "effect/unstable/schema/Model#Override" note: "Moved with the same explicit-default override purpose." diff --git a/repos/effect/migration/annotations/effect__typeclass__Bounded.yaml b/repos/effect/migration/annotations/effect__typeclass__Bounded.yaml index f8389cbb39..04a30c2682 100644 --- a/repos/effect/migration/annotations/effect__typeclass__Bounded.yaml +++ b/repos/effect/migration/annotations/effect__typeclass__Bounded.yaml @@ -10,6 +10,12 @@ "@effect/typeclass/Bounded#clamp": replacement: "Order.clamp(B.compare)" note: "Use the v4 Order combinator with { minimum: B.minBound, maximum: B.maxBound }; the Bounded dictionary itself was removed." +"@effect/typeclass/Bounded#max": + replacement: "Reducer.make(Combiner.max(B.compare).combine, B.minBound)" + note: "V4 removed Bounded dictionaries. Build the maximum Reducer from the separately retained Order and minimum bound." +"@effect/typeclass/Bounded#min": + replacement: "Reducer.make(Combiner.min(B.compare).combine, B.maxBound)" + note: "V4 removed Bounded dictionaries. Build the minimum Reducer from the separately retained Order and maximum bound." "@effect/typeclass/Bounded#reverse": replacement: "Order.flip(B.compare)" note: "Flip the Order and swap the separately stored minimum and maximum bounds; v4 has no bundled Bounded dictionary." diff --git a/repos/effect/migration/annotations/effect__typeclass__Monoid.yaml b/repos/effect/migration/annotations/effect__typeclass__Monoid.yaml index 5d3e62d0a7..d40e6f5025 100644 --- a/repos/effect/migration/annotations/effect__typeclass__Monoid.yaml +++ b/repos/effect/migration/annotations/effect__typeclass__Monoid.yaml @@ -4,6 +4,12 @@ "@effect/typeclass/Monoid#fromSemigroup": replacement: "Reducer.make(S.combine, empty)" note: "Construct a v4 Reducer from the replacement Combiner operation and identity value." +"@effect/typeclass/Monoid#max": + replacement: "Reducer.make(Combiner.max(B.compare).combine, B.minBound)" + note: "Reducer replaces Monoid. Build it from the v4 maximum Combiner and the bounded order's minimum value." +"@effect/typeclass/Monoid#min": + replacement: "Reducer.make(Combiner.min(B.compare).combine, B.maxBound)" + note: "Reducer replaces Monoid. Build it from the v4 minimum Combiner and the bounded order's maximum value." "@effect/typeclass/Monoid#Monoid": replacement: "Reducer.Reducer" note: "Reducer replaces Monoid in v4; empty is renamed initialValue and combineAll remains available." diff --git a/repos/effect/migration/annotations/effect__typeclass__Semigroup.yaml b/repos/effect/migration/annotations/effect__typeclass__Semigroup.yaml index eaab3e72a5..e10dd882e1 100644 --- a/repos/effect/migration/annotations/effect__typeclass__Semigroup.yaml +++ b/repos/effect/migration/annotations/effect__typeclass__Semigroup.yaml @@ -22,6 +22,12 @@ "@effect/typeclass/Semigroup#make": replacement: "Combiner.make" note: "Combiner replaces Semigroup. V4 accepts only the binary combine function and has no combineMany override." +"@effect/typeclass/Semigroup#max": + replacement: "Combiner.max" + note: "Combiner replaces Semigroup; pass the same Order to retain last-maximum tie behavior." +"@effect/typeclass/Semigroup#min": + replacement: "Combiner.min" + note: "Combiner replaces Semigroup; pass the same Order to retain last-minimum tie behavior." "@effect/typeclass/Semigroup#Product": replacement: "none" note: "The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions." diff --git a/repos/effect/migration/annotations/effect__vitest__index.yaml b/repos/effect/migration/annotations/effect__vitest__index.yaml index a40e32ca26..515c9201aa 100644 --- a/repos/effect/migration/annotations/effect__vitest__index.yaml +++ b/repos/effect/migration/annotations/effect__vitest__index.yaml @@ -18,88 +18,88 @@ note: "This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route." "@effect/vitest/index#ApiConfig": replacement: "vitest/node#ApiConfig" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#BaseCoverageOptions": replacement: "vitest/node#BaseCoverageOptions" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#BenchmarkUserOptions": replacement: "vitest/node#BenchmarkUserOptions" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#BrowserConfigOptions": replacement: "vitest/node#BrowserConfigOptions" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#BrowserScript": replacement: "vitest/node#BrowserScript" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#BuiltinEnvironment": replacement: "vitest/node#BuiltinEnvironment" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#CoverageIstanbulOptions": replacement: "vitest/node#CoverageIstanbulOptions" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#CoverageOptions": replacement: "vitest/node#CoverageOptions" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#CoverageProvider": replacement: "vitest/node#CoverageProvider" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#CoverageProviderModule": replacement: "vitest/node#CoverageProviderModule" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#CoverageReporter": replacement: "vitest/node#CoverageReporter" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#CoverageV8Options": replacement: "vitest/node#CoverageV8Options" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#CSSModuleScopeStrategy": replacement: "vitest/node#CSSModuleScopeStrategy" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#CustomProviderOptions": replacement: "vitest/node#CustomProviderOptions" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#DepsOptimizationOptions": replacement: "vitest/node#DepsOptimizationOptions" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#EnvironmentOptions": replacement: "vitest/node#EnvironmentOptions" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#InlineConfig": replacement: "vitest/node#InlineConfig" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#Pool": replacement: "vitest/node#Pool" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#ProjectConfig": replacement: "vitest/node#ProjectConfig" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#ReportContext": replacement: "vitest/node#ReportContext" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#ResolvedConfig": replacement: "vitest/node#ResolvedConfig" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#ResolvedCoverageOptions": replacement: "vitest/node#ResolvedCoverageOptions" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#SequenceHooks": replacement: "vitest/node#SequenceHooks" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#SequenceSetupFiles": replacement: "vitest/node#SequenceSetupFiles" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#TypecheckConfig": replacement: "vitest/node#TypecheckConfig" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#VitestEnvironment": replacement: "vitest/node#VitestEnvironment" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#VitestRunMode": replacement: "vitest/node#VitestRunMode" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#WorkerContext": replacement: "vitest/node#WorkerContext" - note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#CollectLineNumbers": replacement: "vitest/node#TypeCheckCollectLineNumbers" note: "Vitest 3 deprecated the root alias in favor of this renamed vitest/node type." @@ -147,25 +147,25 @@ note: "Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner* type from vitest." "@effect/vitest/index#ExtendedContext": replacement: "vitest#TestContext" - note: "The separate context alias was removed. Vitest 4 uses TestContext, which includes the current task and lifecycle methods." + note: "The separate context alias was removed. Vitest 5 uses TestContext, which includes the current task and lifecycle methods." "@effect/vitest/index#TaskContext": replacement: "vitest#TestContext" - note: "The separate context alias was removed. Vitest 4 uses TestContext, which includes the current task and lifecycle methods." + note: "The separate context alias was removed. Vitest 5 uses TestContext, which includes the current task and lifecycle methods." "@effect/vitest/index#Environment": - replacement: "vitest/environments#Environment" - note: "This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments." + replacement: "vitest/runtime#Environment" + note: "This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime." "@effect/vitest/index#EnvironmentReturn": - replacement: "vitest/environments#EnvironmentReturn" - note: "This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments." + replacement: "vitest/runtime#EnvironmentReturn" + note: "This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime." "@effect/vitest/index#VmEnvironmentReturn": - replacement: "vitest/environments#VmEnvironmentReturn" - note: "This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments." + replacement: "vitest/runtime#VmEnvironmentReturn" + note: "This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime." "@effect/vitest/index#HappyDOMOptions": replacement: "NonNullable" - note: "Vitest 4 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type." + note: "Vitest 5 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type." "@effect/vitest/index#JSDOMOptions": replacement: "NonNullable" - note: "Vitest 4 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type." + note: "Vitest 5 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type." "@effect/vitest/index#ArgumentsType": replacement: "T extends (...args: infer A) => any ? A : never" note: "Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals." @@ -191,38 +191,38 @@ replacement: "vitest#SerializedTestSpecification" note: "Use the non-deprecated Vitest name; SerializableSpec was only an alias." "@effect/vitest/index#Reporter": - replacement: "vitest/reporters#Reporter" - note: "Import Reporter from the public plural vitest/reporters entrypoint; its lifecycle methods changed in Vitest 4." + replacement: "vitest/node#Reporter" + note: "Import Reporter from vitest/node; the deprecated vitest/reporters entrypoint was removed in Vitest 5." "@effect/vitest/index#UserConfig": replacement: "vitest/config#TestUserConfig" - note: "Vitest 4 exposes its config as TestUserConfig; ViteUserConfig is the separate Vite configuration type." + note: "Vitest 5 exposes its config as TestUserConfig; ViteUserConfig is the separate Vite configuration type." "@effect/vitest/index#UserWorkspaceConfig": replacement: "vitest/config#UserWorkspaceConfig" note: "Import the type from vitest/config and migrate Vitest workspace configuration to projects." "@effect/vitest/index#PoolOptions": replacement: "vitest/config#TestUserConfig" - note: "The v3 built-in poolOptions object was removed. Move its fields to Vitest 4 top-level config such as maxWorkers and vmMemoryLimit; vitest/node PoolOptions is a different custom-pool API." + note: "The v3 built-in poolOptions object was removed. Move its fields to Vitest 5 top-level config such as maxWorkers and vmMemoryLimit; vitest/node PoolOptions is a different custom-pool API." "@effect/vitest/index#RuntimeContext": - replacement: "@vitest/runner#RuntimeContext" - note: "Custom-runner code can add an explicit @vitest/runner dependency; ordinary tests should avoid this internal state type." + replacement: "none" + note: "Vitest 5 deprecates @vitest/runner and does not expose this internal state type. Extend TestRunner from vitest and use its public methods instead." "@effect/vitest/index#SuiteHooks": - replacement: "@vitest/runner#SuiteHooks" - note: "Custom-runner code can add an explicit @vitest/runner dependency; ordinary tests should use public hook functions." + replacement: "ReturnType" + note: "Derive the hook collection from Vitest 5's public TestRunner API; ordinary tests should use public hook functions." "@effect/vitest/index#DoneCallback": replacement: "none" note: "Vitest does not support callback-style tests. Return a Promise or, in @effect/vitest tests, return an Effect." "@effect/vitest/index#HookCleanupCallback": replacement: "none" - note: "No named Vitest 4 export replaces this alias. Let the hook return type infer, or type the cleanup function locally." + note: "No named Vitest 5 export replaces this alias. Let the hook return type infer, or type the cleanup function locally." "@effect/vitest/index#HookListener": replacement: "none" - note: "Use the matching @vitest/runner hook-specific type such as BeforeAllListener, AfterAllListener, BeforeEachListener, or AfterEachListener for custom runner code." + note: "Infer the callback from the public hook function, or derive it with Parameters[0] and the corresponding hook name." "@effect/vitest/index#ModuleCache": replacement: "none" - note: "Vitest 3 marked this unused internal cache shape deprecated; Vitest 4 has no public replacement." + note: "Vitest 3 marked this unused internal cache shape deprecated; Vitest 5 has no public replacement." "@effect/vitest/index#ResolvedTestEnvironment": replacement: "none" - note: "Vitest 3 marked this type unsupported. Use Environment from vitest/environments for custom environments." + note: "Vitest 3 marked this type unsupported. Use Environment from vitest/runtime for custom environments." "@effect/vitest/index#ResolveIdFunction": replacement: "none" note: "This deprecated vite-node callback was removed. Use Vite environment or module-runner APIs." @@ -232,3 +232,48 @@ "@effect/vitest/index#WorkerRPC": replacement: "none" note: "The concrete worker RPC composition is internal. Use public Vitest RuntimeRPC, RunnerRPC, ContextRPC, or WorkerRequest types only when their narrower contract fits." +"@effect/vitest/index#bench": + replacement: "vitest#test" + note: "Vitest 5 removes the top-level bench export. Destructure bench from a regular test's context and await bench(name, fn).run(); use skip, only, or todo on the enclosing test." +"@effect/vitest/index#BenchFactory": + replacement: "vitest#Bench" + note: "Use the Vitest 5 test-context bench fixture type. It is no longer the tinybench factory constructor." +"@effect/vitest/index#BenchFunction": + replacement: "vitest#BenchFn" + note: "Use BenchFn for the callback passed to the Vitest 5 test-context bench fixture." +"@effect/vitest/index#BenchTask": + replacement: "vitest#BenchRegistration" + note: "Migrate to a fixture registration and await its run() method; review its fields instead of treating it as a tinybench task." +"@effect/vitest/index#BenchTaskResult": + replacement: "vitest#BenchResult" + note: "Use the result returned by awaiting the Vitest 5 fixture registration's run() method." +"@effect/vitest/index#Benchmark": + replacement: "vitest#TestBenchmark" + note: "Use TestBenchmark for recorded benchmark data on a test; benchmarks are no longer standalone test tasks." +"@effect/vitest/index#BenchmarkAPI": + replacement: "vitest#Bench" + note: "Use the test-context bench fixture. Move skip, only, and todo to the enclosing test." +"@effect/vitest/index#BenchmarkResult": + replacement: "vitest#BenchResult" + note: "Use the result returned by awaiting the Vitest 5 fixture registration's run() method; review its changed fields." +"@effect/vitest/index#BenchmarkRunner": + replacement: "vitest#BenchmarkProvider" + note: "Vitest 5 runs benchmarks through providers. Export a BenchmarkProvider with an asynchronous run(group) method for a custom engine." +"@effect/vitest/index#ExpectPollOptions": + replacement: "NonNullable[1]>" + note: "Vitest 5 removes the named options export; derive the options from the public expect.poll function." +"@effect/vitest/index#Assertion": + replacement: "vitest#Assertion" + note: "Vitest 5 takes the matcher return type first. Replace Assertion with Assertion or Assertion, T> for asynchronous assertions." +"@effect/vitest/index#Matchers": + replacement: "vitest#Matchers" + note: "Augment vitest.Matchers for custom matchers. R is the matcher return type and T is the received value; @vitest/expect no longer shares Vitest's assertion state." +"@effect/vitest/index#it": + replacement: "@effect/vitest#it" + note: "Effect helpers retain their calling convention. Vitest 5 removes it.sequential; pass { concurrent: false } to the native test or as the Effect helper's third argument." +"@effect/vitest/index#test": + replacement: "vitest#test" + note: "Vitest 5 removes test.sequential and sequential options. Pass { concurrent: false } to opt out of inherited concurrency." +"@effect/vitest/index#describe": + replacement: "vitest#describe" + note: "Vitest 5 removes describe.sequential and sequential options. Use describe(name, { concurrent: false }, body) for suites that depend on ordering." diff --git a/repos/effect/migration/annotations/effect__workflow__DurableDeferred.yaml b/repos/effect/migration/annotations/effect__workflow__DurableDeferred.yaml index f30fc24b4d..6e01c63c9a 100644 --- a/repos/effect/migration/annotations/effect__workflow__DurableDeferred.yaml +++ b/repos/effect/migration/annotations/effect__workflow__DurableDeferred.yaml @@ -15,7 +15,7 @@ note: "Moved into core Effect and now requires the error schema encoding services." "@effect/workflow/DurableDeferred#into": replacement: "effect/unstable/workflow/DurableDeferred#into" - note: "Moved into core Effect with the same exit recording and suspension propagation behavior." + note: "Moved into core Effect with the same exit recording and suspension propagation behavior. Provide both decoding and encoding services for the success and error schemas; recording the exit requires encoding services." "@effect/workflow/DurableDeferred#make": replacement: "effect/unstable/workflow/DurableDeferred#make" note: "Moved into core Effect with the same name and optional schemas, expressed through v4 Schema.Constraint." diff --git a/repos/effect/migration/v3-to-v4.md b/repos/effect/migration/v3-to-v4.md index 1d6c697be5..767e4acbbb 100644 --- a/repos/effect/migration/v3-to-v4.md +++ b/repos/effect/migration/v3-to-v4.md @@ -2,9 +2,9 @@ # v3 to v4 Migration Reference -Base: `3d390f232bdbc3f0d3d6a2ae3c775084f494b547` (`3d390f232bdbc3f0d3d6a2ae3c775084f494b547`) +Base: `origin/v3` (`2e471d9cec31889cd6548aa5423b64c2b85238be`) -Head: `origin/main` (`20cb4f260e45d37fa417c292c57be015314efe16`) +Head: `agent/bob/738d126b22f9` (`b4773df70eb78ac7c222b236ff5a0605e6632ac6`) This file is generated from the API diff and `migration/annotations/*.yaml`. @@ -31,7 +31,7 @@ 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/FastCheck -> fast-check 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) @@ -84,7 +84,7 @@ effect/TestClock -> effect/testing/TestClock (barrel: effect/testing) @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/MsgPack -> effect/unstable/encoding/SchemaBinary (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) @@ -506,7 +506,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `@effect/platform-node/NodeContext`: No single module replacement; follow the curated per-API guidance below. - `@effect/platform-node/NodeFileSystem/ParcelWatcher`: No single module replacement; follow the curated per-API guidance below. - `@effect/platform-node/NodeKeyValueStore` -> `effect/unstable/persistence/KeyValueStore`: layerFileSystem is now platform-neutral as KeyValueStore.layerFileSystem(directory); provide FileSystem and Path via NodeServices.layer or NodeFileSystem.layer with NodePath.layer. -- `@effect/platform-node/index` -> `@effect/platform-node`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/platform-node package root or import specific modules directly. +- `@effect/platform-node/index` -> `@effect/platform-node`: The explicit /index entrypoint was removed; import Node-prefixed namespaces from the @effect/platform-node package root or import specific modules directly. Undici is no longer in the root barrel; import it from @effect/platform-node/Undici or directly from undici. - `@effect/platform/ChannelSchema` -> `effect/ChannelSchema` - `@effect/platform/Command` -> `effect/unstable/process/ChildProcess` - `@effect/platform/CommandExecutor` -> `effect/unstable/process/ChildProcessSpawner` @@ -548,7 +548,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `@effect/platform/HttpServerResponse` -> `effect/unstable/http/HttpServerResponse` - `@effect/platform/HttpTraceContext` -> `effect/unstable/http/HttpTraceContext` - `@effect/platform/KeyValueStore` -> `effect/unstable/persistence/KeyValueStore` -- `@effect/platform/MsgPack` -> `effect/unstable/encoding/Msgpack` +- `@effect/platform/MsgPack` -> `effect/unstable/encoding/SchemaBinary`: MessagePack support was removed. Schema-aware encode, decode, and duplex now live on SchemaBinary. Untyped MessagePack of unknown values has no replacement. - `@effect/platform/Multipart` -> `effect/unstable/http/Multipart` - `@effect/platform/Ndjson` -> `effect/unstable/encoding/Ndjson` - `@effect/platform/OpenApi` -> `effect/unstable/httpapi/OpenApi` @@ -682,14 +682,14 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `@effect/workflow/WorkflowEngine` -> `effect/unstable/workflow/WorkflowEngine` - `@effect/workflow/WorkflowProxy` -> `effect/unstable/workflow/WorkflowProxy` - `@effect/workflow/WorkflowProxyServer` -> `effect/unstable/workflow/WorkflowProxyServer` -- `effect/Arbitrary`: No single module replacement; follow the curated per-API guidance below. +- `effect/Arbitrary` -> `effect/unstable/arbitrary/Arbitrary`: Schema-derived generation moved to the native Arbitrary module. Effect no longer bridges to fast-check. - `effect/ChildExecutorDecision` -> `none`: Removed with the v3 channel executor and Channel.concatMapWithCustom. Choose Channel.flatMap, Channel.switchMap, or Channel.mergeAll instead; v4 exposes no child-executor decision ADT. - `effect/ConfigError`: No single module replacement; follow the curated per-API guidance below. - `effect/ConfigProviderPathPatch`: No single module replacement; follow the curated per-API guidance below. - `effect/DefaultServices`: No single module replacement; follow the curated per-API guidance below. - `effect/Either` -> `effect/Result` - `effect/ExecutionStrategy`: No single module replacement; follow the curated per-API guidance below. -- `effect/FastCheck` -> `effect/testing/FastCheck` +- `effect/FastCheck` -> `fast-check`: Effect no longer re-exports fast-check. Depend on the fast-check package and import it directly. For Schema-derived generation, use Arbitrary.schema from effect/unstable/arbitrary. - `effect/FiberId`: No single module replacement; follow the curated per-API guidance below. - `effect/FiberRef` -> `effect/References` - `effect/FiberRefs`: No single module replacement; follow the curated per-API guidance below. @@ -4898,6 +4898,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `AiError.AiError` -> `AiError.AiError`: Moved to effect/unstable/ai/AiError and redesigned from a union of separately tagged errors into one AiError wrapper with a semantic reason. Construct it with AiError.make({ module, method, reason }) and match error.reason rather than the old top-level tags. +- `AiError.HttpRequestDetails` -> `AiError.HttpRequestDetails`: Retained in effect/unstable/ai/AiError and also re-exported as Response.HttpRequestDetails. Hash is an optional string instead of Option, headers may contain Redacted strings, and method includes TRACE. + - `AiError.HttpRequestError` -> `AiError.make + AiError.NetworkError`: Replace the old top-level request error with an AiError whose reason is NetworkError. NetworkError.fromRequestError converts a v4 HttpClientError.RequestError. - `AiError.HttpResponseError` -> `AiError.make + AiError.reasonFromHttpStatus / AiError.InvalidOutputError`: There is no single v4 response-error class. Wrap a semantic reason with AiError.make: use reasonFromHttpStatus for status failures and InvalidOutputError for decode or empty-body failures. @@ -4912,6 +4914,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/ai/EmbeddingModel` +- `EmbeddingModel.Result`: TODO: needs guidance + - `EmbeddingModel.makeDataLoader` -> `EmbeddingModel.make + RequestResolver.setDelay + RequestResolver.batchN`: The dedicated data-loader constructor was removed. EmbeddingModel.make batches concurrent embed requests through its resolver; compose the exposed resolver with setDelay and optional batchN for the old window and maximum-batch behavior. ### `@effect/ai/IdGenerator` @@ -4992,6 +4996,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Response.ToolCallPartEncoded` -> `Response.ToolCallPartEncoded`: Moved to effect/unstable/ai/Response; providerName was removed while providerExecuted remains optional when encoded. +- `Response.ToolResultPart` -> `Response.ToolResultPart`: The schema selects success or failure using isFailure rather than trying both result schemas. It returns Schema.Codec; supply decoding services when decoding and encoding services when encoding. + - `Response.ToolResultPartEncoded` -> `Response.ToolResultPartEncoded`: Moved to effect/unstable/ai/Response; providerName was removed and optional preliminary was added to the encoded shape. - `Response.documentSourcePart` -> `Response.makePart("source", { ...params, sourceType: "document" })`: The lowercase convenience constructor was removed. The DocumentSourcePart model remains, and the generic constructor now requires the document source discriminator. @@ -5046,6 +5052,10 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Tool.Requirements` -> `Tool.HandlerServices`: Renamed and refined. HandlerServices combines parameter-decoding, result-encoding, and request-level dependencies required by a tool handler. +- `Tool.Result` -> `Tool.Result`: The result includes success, declared failure, and execution-denied or execution-interrupted values in both failure modes. Return mode also includes AiError; handle these variants when narrowing results. + +- `Tool.ResultEncoded` -> `Tool.ResultEncoded`: The encoded result includes execution-denied and execution-interrupted variants in both failure modes, plus encoded AiError in return mode. + - `Tool.Success` -> `Tool.Success`: Moved to effect/unstable/ai/Tool and remains the utility type that extracts a tool's decoded success type. - `Tool.Tool.ProviderDefinedProto` -> `Tool.ProviderDefined`: This implementation-brand interface is no longer public. Use Tool.ProviderDefined for the model type and Tool.isProviderDefined for runtime narrowing. @@ -5078,9 +5088,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Args.Args.BaseArgsConfig` -> `name: string`: Argument constructors now take the name as a required first parameter. -- `Args.Args.FormatArgsConfig` -> `Primitive.FileParseOptions`: Pass the name separately and use the format option with Argument.fileParse or Argument.fileSchema. +- `Args.Args.FormatArgsConfig` -> `Primitive.FileParseOptions`: Pass the name separately and use the format option with Argument.FileParse or Argument.FileSchema. -- `Args.Args.PathArgsConfig` -> `Argument.path(name, { pathType, mustExist })`: Path options are inline; map exists=yes to mustExist=true and either to omission. exists=no has no exact replacement. +- `Args.Args.PathArgsConfig` -> `Argument.Path(name, { pathType, mustExist })`: Path options are inline; map exists=yes to mustExist=true and either to omission. exists=no has no exact replacement. - `Args.Args.Variance` -> `Argument.Argument`: The separate variance artifact was removed; Argument inherits the shared Param variance. @@ -5094,9 +5104,25 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Args.between` -> `Argument.between`: Use the moved combinator; v4 validates bounds when constructing the parameter. -- `Args.boolean` -> `Flag.boolean / Argument.choiceWithValue`: Positional booleans were removed as ambiguous; prefer a boolean flag or explicit true/false positional choices. +- `Args.boolean` -> `Flag.Boolean / Argument.ChoiceWithValue`: Positional booleans were removed as ambiguous; prefer a boolean flag or explicit true/false positional choices. + +- `Args.choice` -> `Argument.Literals`: Use the renamed constructor and pass the argument name explicitly. + +- `Args.date` -> `Argument.Date`: Use the renamed constructor and pass the argument name explicitly. + +- `Args.directory` -> `Argument.Directory`: Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement. + +- `Args.file` -> `Argument.File`: Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement. + +- `Args.fileContent` -> `Argument.File + Argument.mapEffect`: Parse a path and read it with FileSystem.readFile; no binary-content argument constructor remains. -- `Args.fileContent` -> `Argument.file + Argument.mapEffect`: Parse a path and read it with FileSystem.readFile; no binary-content argument constructor remains. +- `Args.fileParse` -> `Argument.FileParse`: Pass the old format as an options field; v4 returns parsed content rather than a path/content tuple. + +- `Args.fileSchema` -> `Argument.FileSchema`: Pass the old format as an options field and use a v4 Schema constraint decoder. + +- `Args.fileText` -> `Argument.FileText`: Use the renamed constructor; it returns content only. + +- `Args.float` -> `Argument.Finite`: Use the renamed constructor; it rejects non-finite numbers. - `Args.getHelp` -> `none`: Per-argument help introspection was removed; Command generates help internally. @@ -5108,17 +5134,27 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Args.getUsage` -> `none`: The public Usage tree was removed; Command generates a usage string internally. +- `Args.integer` -> `Argument.Int`: Use the renamed constructor and pass the argument name explicitly. + - `Args.isArgs` -> `Param.isParam(value) && value.kind === Param.argumentKind`: Arguments now use the shared Param representation and an explicit kind discriminator. - `Args.map` -> `Argument.map`: Use the moved combinator. +- `Args.mapEffect`: TODO: needs guidance + +- `Args.none` -> `omit the config entry`: V4 Argument.Never is an always-failing sentinel, not v3's empty successful argument set. + - `Args.optional` -> `Argument.optional`: Use the moved combinator; it still returns Option. +- `Args.path` -> `Argument.Path`: Path options are inline; map exists=yes to mustExist=true and either to omission. exists=no has no exact replacement. + +- `Args.redacted` -> `Argument.Redacted`: Use the renamed constructor and pass the argument name explicitly. + - `Args.repeated` -> `Argument.variadic`: Renamed to variadic; pass optional min and max bounds. -- `Args.secret` -> `Argument.redacted`: Use Redacted-backed positional input. +- `Args.secret` -> `Argument.Redacted`: Use Redacted-backed positional input. -- `Args.text` -> `Argument.string`: Renamed to string; pass the argument name explicitly. +- `Args.text` -> `Argument.String`: Renamed to String; pass the argument name explicitly. - `Args.validate` -> `argument.parse({ flags: {}, arguments: args })`: Parsing is now a Param method and returns leftover tokens with the value; errors are CliError. @@ -5190,6 +5226,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/cli/Command` +- `Command.Command.Config`: TODO: needs guidance + - `Command.Command.Context` -> `Command.CommandContext`: Renamed to CommandContext. - `Command.Command.ParseConfig` -> `Command.Command.Config.Infer`: Use the v4 command-config inference helper. @@ -5278,7 +5316,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `CommandDirective.UserDefined` -> `none`: The user-defined intermediate directive was removed. -- `CommandDirective.builtIn` -> `GlobalFlag.action`: Define a custom action flag; v4 runners no longer return built-in directives. +- `CommandDirective.builtIn` -> `GlobalFlag.Action`: Define a custom action flag; v4 runners no longer return built-in directives. - `CommandDirective.isBuiltIn` -> `none`: Intermediate built-in directives were removed. @@ -5358,7 +5396,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Options.Options` -> `Flag.Flag`: Options was renamed to Flag in effect/unstable/cli. -- `Options.Options.BooleanOptionsConfig` -> `Flag.boolean + Flag.withAlias + Flag.map`: The config object was removed; aliases and value inversion are combinators, while custom negation names need application logic. +- `Options.Options.BooleanOptionsConfig` -> `Flag.Boolean + Flag.withAlias + Flag.map`: The config object was removed; aliases and value inversion are combinators, while custom negation names need application logic. - `Options.Options.PathOptionsConfig` -> `{ readonly mustExist?: boolean }`: Path options are inline; true replaces exists=yes and omission replaces either. exists=no has no exact replacement. @@ -5374,29 +5412,29 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Options.between` -> `Flag.between`: Use the moved combinator; v4 validates bounds when constructing the parameter. -- `Options.boolean` -> `Flag.boolean + Flag.withDefault`: Use Flag.boolean(name).pipe(Flag.withDefault(false)) to preserve v3's omitted-flag default; bare Flag.boolean is now required. --no-name is automatic and aliases are added with Flag.withAlias. +- `Options.boolean` -> `Flag.Boolean + Flag.withDefault`: Use Flag.Boolean(name).pipe(Flag.withDefault(false)) to preserve v3's omitted-flag default; bare Flag.Boolean is now required. --no-name is automatic and aliases are added with Flag.withAlias. -- `Options.choice` -> `Flag.choice`: Use the moved constructor. +- `Options.choice` -> `Flag.Literals`: Use the moved constructor. -- `Options.choiceWithValue` -> `Flag.choiceWithValue`: Use the moved constructor. +- `Options.choiceWithValue` -> `Flag.ChoiceWithValue`: Use the moved constructor. -- `Options.date` -> `Flag.date`: Use the moved constructor. +- `Options.date` -> `Flag.Date`: Use the moved constructor. -- `Options.directory` -> `Flag.directory`: Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement. +- `Options.directory` -> `Flag.Directory`: Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement. -- `Options.file` -> `Flag.file`: Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement. +- `Options.file` -> `Flag.File`: Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement. -- `Options.fileContent` -> `Flag.file + Flag.mapEffect`: Parse a path and read it with FileSystem.readFile; no binary-content flag constructor remains. +- `Options.fileContent` -> `Flag.File + Flag.mapEffect`: Parse a path and read it with FileSystem.readFile; no binary-content flag constructor remains. -- `Options.fileParse` -> `Flag.fileParse`: Pass the old format as an options field; v4 returns parsed content rather than a path/content tuple. +- `Options.fileParse` -> `Flag.FileParse`: Pass the old format as an options field; v4 returns parsed content rather than a path/content tuple. -- `Options.fileSchema` -> `Flag.fileSchema`: Pass the old format as an options field and use a v4 Schema constraint decoder. +- `Options.fileSchema` -> `Flag.FileSchema`: Pass the old format as an options field and use a v4 Schema constraint decoder. -- `Options.fileText` -> `Flag.file + Flag.mapEffect`: Flag.fileText returns content only; read after Flag.file when the path/content tuple must be preserved. +- `Options.fileText` -> `Flag.File + Flag.mapEffect`: Flag.FileText returns content only; read after Flag.File when the path/content tuple must be preserved. - `Options.filterMap` -> `Flag.filterMap`: Use the moved combinator and replace the fixed message with an onNone function. -- `Options.float` -> `Flag.float`: Use the moved constructor. +- `Options.float` -> `Flag.Finite`: Use the moved constructor. - `Options.getHelp` -> `none`: Per-flag help introspection was removed; Command generates help internally. @@ -5404,13 +5442,13 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Options.getUsage` -> `none`: The public Usage tree was removed; Command generates a usage string internally. -- `Options.integer` -> `Flag.integer`: Use the moved constructor. +- `Options.integer` -> `Flag.Int`: Use the moved constructor. - `Options.isBool` -> `none`: No public flag-shape predicate remains; boolean-shape inspection is internal. - `Options.isOptions` -> `Param.isParam(value) && value.kind === Param.flagKind`: Flags now use the shared Param representation and an explicit kind discriminator. -- `Options.keyValueMap` -> `Flag.keyValuePair`: Renamed and now returns Record\ rather than HashMap. +- `Options.keyValueMap` -> `Flag.KeyValuePair`: Renamed and now returns Record\ rather than HashMap. - `Options.map` -> `Flag.map`: Use the moved combinator. @@ -5418,7 +5456,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Options.mapTryCatch` -> `Flag.mapTryCatch`: Use the moved combinator; onError now returns a string rather than HelpDoc. -- `Options.none` -> `omit the config entry`: V4 Flag.none is an always-failing sentinel, not v3's empty successful option set. +- `Options.none` -> `omit the config entry`: V4 Flag.Never is an always-failing sentinel, not v3's empty successful option set. - `Options.optional` -> `Flag.optional`: Use the moved combinator; it still returns Option. @@ -5430,13 +5468,13 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Options.processCommandLine` -> `Command.runWith`: Raw argv processing is now whole-command execution; no public standalone flag tokenizer remains. -- `Options.redacted` -> `Flag.redacted`: Use the moved constructor. +- `Options.redacted` -> `Flag.Redacted`: Use the moved constructor. - `Options.repeated` -> `Flag.variadic`: Renamed to variadic; pass optional min and max bounds. -- `Options.secret` -> `Flag.redacted`: The deprecated Secret constructor was removed; use Redacted-backed input. +- `Options.secret` -> `Flag.Redacted`: The deprecated Secret constructor was removed; use Redacted-backed input. -- `Options.text` -> `Flag.string`: Renamed from text to string. +- `Options.text` -> `Flag.String`: Renamed from text to String. - `Options.withAlias` -> `Flag.withAlias`: Use the moved combinator. @@ -5462,19 +5500,23 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Primitive.PrimitiveTypeId` -> `none`: The public Primitive type-id symbol was removed. -- `Primitive.boolean` -> `Primitive.boolean`: Boolean is now a singleton value; defaults belong on Flag.boolean or withDefault. +- `Primitive.boolean` -> `Primitive.Boolean`: Boolean is now a singleton value; defaults belong on Flag.Boolean or withDefault. + +- `Primitive.choice` -> `Primitive.Choice`: Use the moved constructor. -- `Primitive.choice` -> `Primitive.choice`: Use the moved constructor. +- `Primitive.date` -> `Primitive.Date`: Date is now a singleton Primitive value. -- `Primitive.date` -> `Primitive.date`: Date is now a singleton Primitive value. +- `Primitive.float` -> `Primitive.Finite`: Finite is now a singleton Primitive value and rejects non-finite numbers. - `Primitive.getChoices` -> `none`: Choice introspection is internal in v4; retain alternatives in application code when needed. - `Primitive.getHelp` -> `none`: Primitive-level help generation was removed from the public API. +- `Primitive.integer` -> `Primitive.Int`: Int is now a singleton Primitive value. + - `Primitive.isBool` -> `none`: The boolean Primitive predicate is internal in v4. -- `Primitive.text` -> `Primitive.string`: Renamed from text to string. +- `Primitive.text` -> `Primitive.String`: Renamed from text to String. - `Primitive.validate` -> `primitive.parse(value)`: Parsing is now the Primitive.parse method over a string; defaults and case normalization moved out of this layer. @@ -5486,21 +5528,41 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Prompt.Prompt` -> `Prompt.Prompt`: The model moved to effect/unstable/cli; quitting now fails with Terminal.QuitError. +- `Prompt.Prompt.FloatOptions` -> `Prompt.NumberOptions`: Use the renamed public options type for Prompt.Number; it still extends the integer options type, now IntOptions. + +- `Prompt.Prompt.IntegerOptions` -> `Prompt.IntOptions`: Use the renamed public options type for Prompt.Int; option fields are preserved. + - `Prompt.Prompt.Variance` -> `Prompt.Prompt`: The named variance artifact was removed; use Prompt\. - `Prompt.Prompt.VarianceStruct` -> `Prompt.Prompt`: The named variance structure was removed; use Prompt\. - `Prompt.PromptTypeId` -> `Prompt.isPrompt`: The public type-id symbol was removed; use the runtime guard. -- `Prompt.date` -> `Prompt.date`: Use the moved constructor. +- `Prompt.confirm` -> `Prompt.Confirm`: Use the renamed constructor. + +- `Prompt.custom` -> `Prompt.Custom`: Use the renamed constructor; both overloads are preserved. + +- `Prompt.date` -> `Prompt.Date`: Use the moved constructor. + +- `Prompt.file` -> `Prompt.File`: Use the moved constructor; v4 also supports a default selected path. + +- `Prompt.float` -> `Prompt.Number`: Use the moved constructor; v4 also supports a default value. + +- `Prompt.hidden` -> `Prompt.Hidden`: Use the renamed constructor. + +- `Prompt.integer` -> `Prompt.Int`: Use the moved constructor; v4 also supports a default value. + +- `Prompt.list` -> `Prompt.List`: Use the renamed constructor. -- `Prompt.file` -> `Prompt.file`: Use the moved constructor; v4 also supports a default selected path. +- `Prompt.multiSelect` -> `Prompt.MultiSelect`: Use the renamed constructor. -- `Prompt.float` -> `Prompt.float`: Use the moved constructor; v4 also supports a default value. +- `Prompt.password` -> `Prompt.Password`: Use the renamed constructor. -- `Prompt.integer` -> `Prompt.integer`: Use the moved constructor; v4 also supports a default value. +- `Prompt.select` -> `Prompt.Select`: Use the renamed constructor. -- `Prompt.text` -> `Prompt.text`: Use the moved constructor. +- `Prompt.text` -> `Prompt.String`: Use the moved constructor. + +- `Prompt.toggle` -> `Prompt.Toggle`: Use the renamed constructor. ### `@effect/cli/ValidationError` @@ -5630,9 +5692,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `EntityProxyServer.RpcHandlers` -> `effect/unstable/cluster/EntityProxyServer#RpcHandlers`: Moved into core Effect and updated for the additional v4 Rpc requirements type parameter. -- `EntityProxyServer.layerHttpApi` -> `effect/unstable/cluster/EntityProxyServer#layerHttpApi`: Moved into core Effect. Use v4 HttpApi identifiers and Rpc.ServicesServer requirements. +- `EntityProxyServer.layerHttpApi` -> `effect/unstable/cluster/EntityProxyServer#layerHttpApi`: Moved into core Effect. Use v4 HttpApi identifiers and provide Sharding plus both Rpc.ServicesServer and Rpc.ServicesClient codec requirements. -- `EntityProxyServer.layerRpcHandlers` -> `effect/unstable/cluster/EntityProxyServer#layerRpcHandlers`: Moved into core Effect; the service requirement is now Rpc.ServicesServer rather than Rpc.Context. +- `EntityProxyServer.layerRpcHandlers` -> `effect/unstable/cluster/EntityProxyServer#layerRpcHandlers`: Moved into core Effect; replace Rpc.Context with both Rpc.ServicesServer and Rpc.ServicesClient codec requirements, alongside Sharding. ### `@effect/cluster/EntityResource` @@ -5686,6 +5748,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/cluster/Reply` +- `Reply.Reply` -> `Reply.Reply(rpc, codecFor)`: Pass the transport codec. Decoding replies requires Rpc.ServicesClient; encoding replies requires Rpc.ServicesServer. + - `Reply.ReplyEncoded` -> `effect/unstable/cluster/Reply#Encoded`: Renamed to Encoded and no longer parameterized by an Rpc; payload fields are unknown and validated by Reply.Reply(rpc, codecFor) with the transport's codec. - `Reply.TypeId` -> `none`: The reply marker is private in v4. Use Reply.isReply for runtime refinement. @@ -5860,15 +5924,17 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `EventLogRemote.Hello` -> `effect/unstable/eventlog/EventLogMessage#HelloResponse`: HelloResponse replaces Hello and includes the v4 authentication challenge; HelloRpc defines the endpoint. +- `EventLogRemote.Ping`: TODO: needs guidance + - `EventLogRemote.Pong` -> `none`: The event-log Pong model was removed; heartbeats belong to the generic RPC socket protocol. - `EventLogRemote.ProtocolRequest` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: EventLogRemoteRpcs and generic RPC serialization replace the old protocol request union. -- `EventLogRemote.ProtocolRequestMsgPack` -> `effect/unstable/rpc/RpcSerialization#layerMsgPack`: Use the generic MsgPack RPC serialization layer instead of a request-specific schema. +- `EventLogRemote.ProtocolRequestMsgPack` -> `effect/unstable/rpc/RpcSerialization#layerSchemaBinary`: Use the generic SchemaBinary RPC serialization layer instead of a request-specific schema. - `EventLogRemote.ProtocolResponse` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: EventLogRemoteRpcs and generic RPC serialization replace the old protocol response union. -- `EventLogRemote.ProtocolResponseMsgPack` -> `effect/unstable/rpc/RpcSerialization#layerMsgPack`: Use the generic MsgPack RPC serialization layer instead of a response-specific schema. +- `EventLogRemote.ProtocolResponseMsgPack` -> `effect/unstable/rpc/RpcSerialization#layerSchemaBinary`: Use the generic SchemaBinary RPC serialization layer instead of a response-specific schema. - `EventLogRemote.RemoteAdditions` -> `none`: This unused protocol model has no v4 counterpart. @@ -5876,17 +5942,17 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `EventLogRemote.StopChanges` -> `none`: Interrupt the ChangesRpc stream instead of sending a StopChanges message. -- `EventLogRemote.decodeRequest` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific request decoder. +- `EventLogRemote.decodeRequest` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific request decoder. -- `EventLogRemote.decodeResponse` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific response decoder. +- `EventLogRemote.decodeResponse` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific response decoder. -- `EventLogRemote.encodeRequest` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific request encoder. +- `EventLogRemote.encodeRequest` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific request encoder. -- `EventLogRemote.encodeResponse` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific response encoder. +- `EventLogRemote.encodeResponse` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific response encoder. - `EventLogRemote.fromSocket` -> `effect/unstable/eventlog/EventLogRemote#makeEncrypted + effect/unstable/rpc/RpcClient#makeProtocolSocket`: Construct the encrypted remote separately from its generic RPC socket protocol. -- `EventLogRemote.layerWebSocket` -> `effect/unstable/eventlog/EventLogRemote#layerEncrypted + effect/unstable/rpc/RpcClient#layerProtocolSocket`: Compose the encrypted remote with the generic socket protocol, MsgPack serialization, and a Socket provider. +- `EventLogRemote.layerWebSocket` -> `effect/unstable/eventlog/EventLogRemote#layerEncrypted + effect/unstable/rpc/RpcClient#layerProtocolSocket`: Compose the encrypted remote with the generic socket protocol, SchemaBinary serialization, and a Socket provider. - `EventLogRemote.layerWebSocketBrowser` -> `effect/unstable/eventlog/EventLogRemote#layerEncrypted + effect/unstable/rpc/RpcClient#layerProtocolSocket + @effect/platform-browser/BrowserSocket#layerWebSocket`: Compose the encrypted remote and generic RPC socket protocol with the browser WebSocket layer. @@ -5904,6 +5970,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/experimental/PersistedQueue` +- `PersistedQueue.ErrorTypeId` -> `effect/unstable/persistence/PersistedQueue#ErrorTypeId`: Retained as a string brand; the runtime marker now uses the persistence module path. + - `PersistedQueue.TypeId` -> `effect/unstable/persistence/PersistedQueue#TypeId`: Import TypeId from the v4 unstable PersistedQueue module; it is now a string brand. - `PersistedQueue.make` -> `effect/unstable/persistence/PersistedQueue#make`: Import make from the v4 unstable PersistedQueue module. @@ -5966,6 +6034,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/experimental/RateLimiter` +- `RateLimiter.ErrorTypeId` -> `effect/unstable/persistence/RateLimiter#ErrorTypeId`: Retained as a string brand; the runtime marker now uses the persistence module path. + - `RateLimiter.RateLimiterError` -> `effect/unstable/persistence/RateLimiter#RateLimiterError`: The retained name is now a wrapper error class whose reason is RateLimitExceeded or RateLimitStoreError. - `RateLimiter.TypeId` -> `effect/unstable/persistence/RateLimiter#TypeId`: Import TypeId from the v4 unstable RateLimiter module; it is now a string brand. @@ -6180,7 +6250,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform-bun/BunStream` -- `BunStream.FromReadableOptions` -> `Pick[0], "chunkSize" | "closeOnDone">`: The named interface was inlined into the constructor options; chunkSize is now a number and the full options also contain evaluate, onError, and bufferSize. +- `BunStream.FromReadableOptions` -> `Pick[0], "chunkSize" | "closeOnDone">`: The named interface was inlined into the constructor options; chunkSize is now a number and the full options also contain evaluate and onError. The ignored bufferSize option was removed. - `BunStream.FromWritableOptions` -> `Pick[0], "endOnDone" | "encoding">`: The named interface was inlined into BunSink.fromWritable and duplex constructor options. @@ -6324,6 +6394,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Undici.MessageEventInit` -> `undici.MessageEventInit`: Import the upstream type directly; message ports and source use MessagePort instances in Undici 8. +- `Undici.MockAgent`: TODO: needs guidance + - `Undici.Pool` -> `undici.Pool`: Import the upstream Pool directly and apply interceptors after construction with pool.compose(...). - `Undici.Pool.Options` -> `undici.Pool.Options`: Import the same Pool namespace type; Undici 8 removes the interceptors option in favor of pool.compose(...). @@ -6426,6 +6498,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Cookies.CookieTypeId` -> `Cookies.isCookie`: The cookie brand is private in v4; use the public refinement instead of reading the type-id symbol. +- `Cookies.CookiesError` -> `Cookies.CookiesError`: The error tag is CookiesError rather than CookieError. Update catchTag calls and \_tag comparisons; validation details are in the reason field. + - `Cookies.ErrorTypeId` -> `Cookies.CookiesError`: The error brand is private in v4; identify the exported error class instead. - `Cookies.TypeId` -> `Cookies.isCookies`: The collection brand is private in v4; use the public refinement instead. @@ -6474,21 +6548,35 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `FileSystem.FileTypeId` -> `typeof FileSystem.FileTypeId`: The runtime marker remains exported, but the separate type alias was removed. +- `FileSystem.GiB` -> `ByteSize.gibibytes`: Use the ByteSize binary unit constructor. + +- `FileSystem.KiB` -> `ByteSize.kibibytes`: Use the ByteSize binary unit constructor. + - `FileSystem.MakeDirectoryOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. - `FileSystem.MakeTempDirectoryOptions` -> `NonNullable[0]>`: Operation option interfaces are inline in the v4 FileSystem service. - `FileSystem.MakeTempFileOptions` -> `NonNullable[0]>`: Operation option interfaces are inline in the v4 FileSystem service. +- `FileSystem.MiB` -> `ByteSize.mebibytes`: Use the ByteSize binary unit constructor. + - `FileSystem.OpenFileOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. +- `FileSystem.PiB` -> `ByteSize.pebibytes`: Use the ByteSize binary unit constructor. + - `FileSystem.ReadDirectoryOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. - `FileSystem.RemoveOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. - `FileSystem.SinkOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. -- `FileSystem.StreamOptions` -> `NonNullable[1]>`: Stream options are inline; bufferSize was removed while bytesToRead, chunkSize, and offset remain. +- `FileSystem.Size` -> `ByteSize.ByteSize`: Use ByteSize.bytes or unit constructors for file sizes. Truncation lengths, buffer sizes, and read/write counts use number. File.seek takes and returns signed bigint positions; it can fail with PlatformError, including BadArgument when seeking before the start. + +- `FileSystem.SizeInput` -> `ByteSize.Input`: File-size and path-backed range inputs use ByteSize.Input. Truncation lengths, Web File ranges, and buffer sizes use number. + +- `FileSystem.StreamOptions` -> `NonNullable[1]>`: Stream options are inline; bufferSize was removed, bytesToRead and offset accept ByteSize inputs, and chunkSize uses number. + +- `FileSystem.TiB` -> `ByteSize.tebibytes`: Use the ByteSize binary unit constructor. - `FileSystem.WatchEventCreate` -> `FileSystem.WatchEvent.Create`: The constructor was removed; construct a tagged object with \_tag: "Create" and path. @@ -6516,9 +6604,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Headers.remove` -> `Headers.remove / Headers.removeMany`: Use remove for one name or removeMany for an iterable; RegExp removal requires enumerating matching names. -- `Headers.schema` -> `Headers.HeadersSchema`: The encoded-record and self schemas were consolidated into HeadersSchema. +- `Headers.schema` -> `Schema.Headers`: The encoded-record and self schemas were consolidated and moved to effect/Schema as Schema.Headers. -- `Headers.schemaFromSelf` -> `Headers.HeadersSchema`: The encoded-record and self schemas were consolidated into HeadersSchema. +- `Headers.schemaFromSelf` -> `Schema.Headers`: The encoded-record and self schemas were consolidated and moved to effect/Schema as Schema.Headers. - `Headers.unsafeFromRecord` -> `Headers.fromRecordUnsafe`: Renamed to put Unsafe last; it still skips name normalization. @@ -6546,6 +6634,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `HttpApiBuilder.HandlersTypeId` -> `none`: The exported symbol was removed; do not inspect or construct the private Handlers marker. +- `HttpApiBuilder.Middleware` -> `none`: The API-specific middleware service tag was removed. Declared HttpApiMiddleware services are applied while routes are built; use HttpRouter.middleware for additional global middleware. + - `HttpApiBuilder.MiddlewareFn` -> `effect/unstable/http/HttpRouter#middleware.Fn`: HTTP apps are Effects in v4; use the router middleware function type or infer it through HttpRouter.middleware. - `HttpApiBuilder.Router` -> `effect/unstable/http/HttpRouter#HttpRouter`: The API-specific router tag was removed; API and group layers register with the shared HttpRouter service. @@ -6556,7 +6646,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `HttpApiBuilder.group` -> `effect/unstable/httpapi/HttpApiBuilder#group`: The group layer remains; names are now identifiers and API/group global error channels are gone. -- `HttpApiBuilder.handler` -> `effect/unstable/httpapi/HttpApiBuilder#endpoint`: Use endpoint for a standalone typed endpoint implementation; inside a group pass callbacks to handlers.handle. +- `HttpApiBuilder.handler` -> `effect/unstable/httpapi/HttpApiBuilder#handler`: The typed callback helper remains; names are now identifiers and API/group global error channels are gone. Pass the returned callback to handlers.handle. - `HttpApiBuilder.httpApp` -> `effect/unstable/http/HttpRouter#toHttpEffect`: Build the application from the assembled API route layer; HTTP apps are Effects in v4. @@ -6866,9 +6956,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `HttpBody.Uint8Array` -> `HttpBody.Uint8Array`: Retained with the same fields and tag, but v4 exports a class. -- `HttpBody.file` -> `HttpBody.file`: Retained; bufferSize was replaced by chunkSize and the other file options remain. +- `HttpBody.file` -> `HttpBody.file`: Retained; bufferSize was replaced by numeric chunkSize. Offset and bytesToRead accept ByteSize.Input. Invalid ranges and a final EOF-clamped content length above Number.MAX\_SAFE\_INTEGER fail with PlatformError / BadArgument. -- `HttpBody.fileInfo` -> `HttpBody.fileFromInfo`: Renamed; it still uses supplied File.Info for content length and requires FileSystem. +- `HttpBody.fileInfo` -> `HttpBody.fileFromInfo`: Renamed; it uses supplied File.Info with ByteSize size metadata and requires FileSystem. Offset and bytesToRead accept ByteSize.Input, while chunkSize is numeric. Invalid ranges and a final EOF-clamped content length above Number.MAX\_SAFE\_INTEGER fail with PlatformError / BadArgument. - `HttpBody.unsafeJson` -> `HttpBody.jsonUnsafe`: Renamed to put Unsafe last; serialization failures still throw. @@ -6940,6 +7030,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/HttpClientResponse` +- `HttpClientResponse.HttpClientResponse` -> `HttpClientResponse.HttpClientResponse`: Custom implementations must supply the required url string. It represents the resolved URL including query parameters and excluding the hash, uses the final URL after redirects, and is empty when unknown. + - `HttpClientResponse.TypeId` -> `typeof HttpClientResponse.TypeId`: TypeId remains public but is now a string constant; use typeof in type position. - `HttpClientResponse.filterStatus` -> `HttpClientResponse.filterStatus`: Retained; rejected status now fails with an HttpClientError wrapper. @@ -6960,11 +7052,11 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/HttpIncomingMessage` -- `HttpIncomingMessage.MaxBodySize` -> `HttpIncomingMessage.MaxBodySize`: Changed from a Reference subclass holding Option\ to Context.Reference\. +- `HttpIncomingMessage.MaxBodySize` -> `HttpIncomingMessage.MaxBodySize`: Changed from a Reference subclass holding Option\ to Context.Reference\. - `HttpIncomingMessage.TypeId` -> `typeof HttpIncomingMessage.TypeId`: TypeId remains public but is now a string constant; use typeof in type position. -- `HttpIncomingMessage.withMaxBodySize` -> `Effect.provideService(HttpIncomingMessage.MaxBodySize, size)`: The helper was removed; provide FileSystem.Size(input) or undefined directly. +- `HttpIncomingMessage.withMaxBodySize` -> `Effect.provideService(HttpIncomingMessage.MaxBodySize, size)`: The helper was removed; provide a ByteSize value or undefined directly. ### `@effect/platform/HttpLayerRouter` @@ -7034,13 +7126,13 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/HttpPlatform` -- `HttpPlatform.HttpPlatform` -> `HttpPlatform.HttpPlatform`: The service is now a Context.Service class; use its Service member for the implementation type. +- `HttpPlatform.HttpPlatform` -> `HttpPlatform.HttpPlatform`: The service is now a Context.Service class; use its Service member for the implementation type. Path-backed offset and bytesToRead accept ByteSize.Input, while chunkSize and all Web File range options use number. - `HttpPlatform.TypeId` -> `none`: The public type id was removed; use the HttpPlatform Context.Service class. - `HttpPlatform.layer` -> `HttpPlatform.layer`: Retained as the default file-response layer. -- `HttpPlatform.make` -> `HttpPlatform.make`: Retained; v4 returns the service implementation and uses updated file stream options. +- `HttpPlatform.make` -> `HttpPlatform.make`: Retained; v4 returns the service implementation. The fileResponse callback receives contentLength as bigint, while start and end remain numbers. Web File range options use number. ### `@effect/platform/HttpRouter` @@ -7112,12 +7204,18 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/HttpServer` +- `HttpServer.Address` -> `effect/unstable/net/NetAddress#SocketAddress`: Replaced by the shared concrete internet-or-Unix socket address union. + - `HttpServer.HttpServer` -> `HttpServer.HttpServer`: The interface and tag became one Context.Service class; use its Service member for implementations. - `HttpServer.ServeOptions` -> `none`: The unused respond option model was removed with no shared v4 counterpart. +- `HttpServer.TcpAddress` -> `effect/unstable/net/NetAddress#InetAddress`: Replaced by the shared resolved internet-address model; use address and port instead of hostname and port. + - `HttpServer.TypeId` -> `none`: The public TypeId was removed; HttpServer is now a Context.Service class. +- `HttpServer.UnixAddress` -> `effect/unstable/net/NetAddress#UnixPathAddress`: Replaced by the shared Unix filesystem-path address model. + - `HttpServer.addressWith` -> `HttpServer.HttpServer.use(({ address }) => effect(address))`: The accessor was removed; read the service and pass its Address to the callback. - `HttpServer.layerContext` -> `HttpServer.layerServices`: Renamed; it provides the standard HTTP platform services. @@ -7168,7 +7266,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `HttpServerResponse.expireCookie` -> `HttpServerResponse.expireCookie`: Now effectful and safe; use expireCookieUnsafe for synchronous throwing behavior. -- `HttpServerResponse.file` -> `HttpServerResponse.file`: Retained with updated FileSystem stream options. +- `HttpServerResponse.file` -> `HttpServerResponse.file`: Retained; offset and bytesToRead accept ByteSize.Input, while chunkSize uses number. Path-backed responses validate ranges and clamp content length to the available bytes. + +- `HttpServerResponse.fileWeb` -> `HttpServerResponse.fileWeb`: Web File offset, bytesToRead, and chunkSize options use number, unlike path-backed ByteSize.Input ranges. - `HttpServerResponse.isServerResponse` -> `HttpServerResponse.isHttpServerResponse`: Renamed. @@ -7206,21 +7306,23 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/MsgPack` -- `MsgPack.ErrorTypeId` -> `Msgpack.MsgPackError`: The public error type-id alias was removed; use the MsgPackError class. +- `MsgPack.ErrorTypeId` -> `Schema.SchemaError`: MsgPackError was removed with MessagePack. SchemaBinary channels fail with Schema.SchemaError. + +- `MsgPack.MsgPackError` -> `Schema.SchemaError`: MsgPackError was removed with MessagePack. SchemaBinary channels fail with Schema.SchemaError. -- `MsgPack.duplex` -> `Msgpack.duplex`: The API moved to effect/unstable/encoding/Msgpack. +- `MsgPack.duplex` -> `SchemaBinary.duplex`: MessagePack was removed. Wrap a byte channel with SchemaBinary.duplex and explicit input and output schemas. -- `MsgPack.duplexSchema` -> `Msgpack.duplexSchema`: The API moved to effect/unstable/encoding/Msgpack and uses v4 Schema constraints. +- `MsgPack.duplexSchema` -> `SchemaBinary.duplex`: MessagePack was removed. Use SchemaBinary.duplex with the v4 Schema model. -- `MsgPack.pack` -> `Msgpack.encode`: The MessagePack channel constructor was renamed from pack to encode. +- `MsgPack.pack` -> `SchemaBinary.encode`: MessagePack was removed. Encode schema values to binary frames with SchemaBinary.encode. -- `MsgPack.packSchema` -> `Msgpack.encodeSchema`: The schema-aware pack channel was renamed to encodeSchema. +- `MsgPack.packSchema` -> `SchemaBinary.encode`: MessagePack was removed. Use SchemaBinary.encode with the v4 Schema model. -- `MsgPack.schema` -> `Msgpack.schema`: The schema helper remains in the moved module and uses the v4 Schema model. +- `MsgPack.schema` -> `SchemaBinary.toCodec`: MessagePack was removed. Derive a binary codec from a Schema with SchemaBinary.toCodec. -- `MsgPack.unpack` -> `Msgpack.decode`: The MessagePack channel constructor was renamed from unpack to decode. +- `MsgPack.unpack` -> `SchemaBinary.decode`: MessagePack was removed. Decode binary frames with SchemaBinary.decode. -- `MsgPack.unpackSchema` -> `Msgpack.decodeSchema`: The schema-aware unpack channel was renamed to decodeSchema. +- `MsgPack.unpackSchema` -> `SchemaBinary.decode`: MessagePack was removed. Use SchemaBinary.decode with the v4 Schema model. ### `@effect/platform/Multipart` @@ -7230,9 +7332,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Multipart.FileSchema` -> `Multipart.PersistedFileSchema`: The schema for persisted multipart files was renamed. -- `Multipart.MaxFieldSize` -> `Multipart.MaxFieldSize`: The setting remains but is now a Context.Reference. +- `Multipart.MaxFieldSize` -> `Multipart.MaxFieldSize`: Now a Context.Reference\; provide a value such as ByteSize.bytes(100). -- `Multipart.MaxFileSize` -> `Multipart.MaxFileSize`: The setting remains as a Context.Reference; use undefined rather than Option.none for no limit. +- `Multipart.MaxFileSize` -> `Multipart.MaxFileSize`: Now a Context.Reference\; provide ByteSize.bytes(100), for example, or undefined for no limit. - `Multipart.MaxParts` -> `Multipart.MaxParts`: The setting remains as a Context.Reference; use undefined rather than Option.none for no limit. @@ -7246,13 +7348,13 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Multipart.withLimits` -> `Effect.provideContext(effect, Multipart.limitsServices(options))`: Build the multipart limit context and provide it to the effect; Option-valued limits became optional plain values. -- `Multipart.withLimits.Options` -> `Multipart.withLimits.Options`: Limit fields now use optional plain numbers or SizeInput values; convert Option.none to undefined and Option.some(value) to value. +- `Multipart.withLimits.Options` -> `Multipart.withLimits.Options`: Limit fields now use optional plain numbers or ByteSize inputs; convert Option.none to undefined and Option.some(value) to value. - `Multipart.withLimitsStream` -> `Stream.provideContext(stream, Multipart.limitsServices(options))`: Build the multipart limit context and provide it to the stream; Option-valued limits became optional plain values. -- `Multipart.withMaxFieldSize` -> `Effect.provideService(Multipart.MaxFieldSize, size)`: Provide the v4 Context.Reference around the effect. +- `Multipart.withMaxFieldSize` -> `Effect.provideService(Multipart.MaxFieldSize, size)`: Provide a ByteSize value, such as ByteSize.bytes(100). To normalize ByteSize.Input options, use Multipart.limitsServices. -- `Multipart.withMaxFileSize` -> `Effect.provideService(Multipart.MaxFileSize, size)`: Provide the v4 Context.Reference around the effect, converting Option.none to undefined. +- `Multipart.withMaxFileSize` -> `Effect.provideService(Multipart.MaxFileSize, size)`: Replace Option.none with undefined and Option.some(value) with a normalized ByteSize value. To normalize ByteSize.Input options, use Multipart.limitsServices. - `Multipart.withMaxParts` -> `Effect.provideService(Multipart.MaxParts, count)`: Provide the v4 Context.Reference around the effect, converting Option.none to undefined. @@ -7304,6 +7406,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `OpenApiJsonSchema.Array` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces were consolidated into the general object model. +- `OpenApiJsonSchema.Boolean` -> `effect/JsonSchema#JsonSchema`: The narrow boolean interface was consolidated into the open, dialect-neutral JSON Schema object model. + - `OpenApiJsonSchema.Empty` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces and special id shapes were removed. - `OpenApiJsonSchema.Enum` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces were consolidated into the general object model. @@ -7314,6 +7418,10 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `OpenApiJsonSchema.JsonSchema` -> `effect/JsonSchema#JsonSchema`: Use the dialect-neutral open JSON Schema object model. +- `OpenApiJsonSchema.Never` -> `effect/JsonSchema#JsonSchema`: The special never-schema interface was consolidated into the open JSON Schema object model; represent it with a not constraint. + +- `OpenApiJsonSchema.Number` -> `effect/JsonSchema#JsonSchema`: The narrow number interface was consolidated into the open, dialect-neutral JSON Schema object model. + - `OpenApiJsonSchema.Numeric` -> `effect/JsonSchema#JsonSchema`: The narrow numeric interfaces were consolidated into the general object model. - `OpenApiJsonSchema.Object` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces were consolidated into the general object model. @@ -7322,6 +7430,12 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `OpenApiJsonSchema.Root` -> `effect/JsonSchema#MultiDocument`: OpenAPI generation keeps roots in schemas and shared components in definitions; the inline-definitions root model is gone. +- `OpenApiJsonSchema.String` -> `effect/JsonSchema#JsonSchema`: The narrow string interface was consolidated into the open, dialect-neutral JSON Schema object model. + +- `OpenApiJsonSchema.Unknown`: TODO: needs guidance + +- `OpenApiJsonSchema.Void` -> `effect/JsonSchema#JsonSchema`: The special void-schema interface was consolidated into the open JSON Schema object model. + - `OpenApiJsonSchema.make` -> `effect/Schema#toJsonSchemaDocument + effect/JsonSchema#toMultiDocumentOpenApi3_1`: Generate Draft 2020-12, wrap the root in a multi-document, then convert references and definitions to OpenAPI 3.1. - `OpenApiJsonSchema.makeWithDefs` -> `effect/SchemaRepresentation#toJsonSchemaMultiDocument + effect/JsonSchema#toMultiDocumentOpenApi3_1`: Definitions are returned separately; build a multi-document representation and convert it to OpenAPI 3.1. @@ -7362,14 +7476,26 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Socket.WebSocketConstructor` -> `Socket.WebSocketConstructor`: The service moved to effect/unstable/socket/Socket and is now a Context.Service class. -- `Socket.currentSendQueueCapacity` -> `Socket.SendQueueCapacity`: The FiberRef was replaced by a defaulted Context.Reference. +- `Socket.currentSendQueueCapacity` -> `none`: The send queue was removed. The v4 Socket is pull-based: acquire socket.reader in a scope and pull frame batches; writes apply the transport's native backpressure. + +- `Socket.defaultCloseCodeIsError` -> `none`: Sockets no longer classify close codes; every close fails the reader's pull with a SocketError wrapping SocketCloseError. Consumers that treat a close as normal catch the error. + +- `Socket.fromTransformStream` -> `Socket.fromTransformStream`: The constructor remains in effect/unstable/socket/Socket but drops closeCodeIsError; every close fails the reader's pull with a SocketError wrapping SocketCloseError. - `Socket.layerWebSocket` -> `Socket.layerWebSocket`: The constructor remains in effect/unstable/socket/Socket; its URL may now also be an Effect. +- `Socket.toChannelMap` -> `none`: The v4 Socket read side is an Effect that never completes via Cause.Done; map frames by acquiring Socket.readerBytes or Socket.readerString, or Effect.map the reader from socket.reader, and use Socket.toChannel or Socket.toChannelString for duplex channels. + ### `@effect/platform/SocketServer` +- `SocketServer.Address` -> `effect/unstable/net/NetAddress#SocketAddress`: Replaced by the shared concrete internet-or-Unix socket address union. + - `SocketServer.ErrorTypeId` -> `SocketServer.ErrorTypeId`: The API moved to effect/unstable/socket/SocketServer and retains this name. +- `SocketServer.TcpAddress` -> `effect/unstable/net/NetAddress#InetAddress`: Replaced by the shared resolved internet-address model; use address and port instead of hostname and port. + +- `SocketServer.UnixAddress` -> `effect/unstable/net/NetAddress#UnixPathAddress`: Replaced by the shared Unix filesystem-path address model. + ### `@effect/platform/Template` - `Template.Interpolated.Context` -> `Template.Interpolated.Context`: The API moved to effect/unstable/http/Template; v4 interpolation types also account for Effect values. @@ -7394,6 +7520,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/Url` +- `Url.fromString` -> `Url.fromString`: Retained; returns Result with IllegalArgumentError instead of Either with IllegalArgumentException. + - `Url.setUrlParams` -> `Url.setUrlParams`: Retained and widened to accept UrlParams.Input. ### `@effect/platform/UrlParams` @@ -7402,19 +7530,21 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `UrlParams.Input` -> `UrlParams.Input`: Retained and broadened to include UrlParams itself. +- `UrlParams.UrlParams` -> `UrlParams.UrlParams`: Import UrlParams from effect/unstable/http. It is now a branded iterable object with a params field rather than a ReadonlyArray; construct it with UrlParams.make or UrlParams.fromInput. + - `UrlParams.makeUrl` -> `Url.make`: Moved to Url, returns Result, and takes string | undefined for the hash. -- `UrlParams.schemaFromSelf` -> `UrlParams.UrlParamsSchema`: Renamed to the declaration schema for the v4 wrapper. +- `UrlParams.schemaFromSelf` -> `Schema.UrlParams`: The declaration schema for the v4 wrapper moved to effect/Schema. -- `UrlParams.schemaFromString` -> `Schema.String.pipe(Schema.decodeTo(UrlParams.UrlParamsSchema, { decode: SchemaGetter.transform((s) => UrlParams.fromInput(new URLSearchParams(s))), encode: SchemaGetter.transform(UrlParams.toString) }))`: No prebuilt string codec remains; recreate it by transforming between a query string and UrlParams. +- `UrlParams.schemaFromString` -> `Schema.String.pipe(Schema.decodeTo(Schema.UrlParams, { decode: SchemaGetter.transform((s) => UrlParams.fromInput(new URLSearchParams(s))), encode: SchemaGetter.transform(UrlParams.toString) }))`: No prebuilt string codec remains; recreate it by transforming between a query string and UrlParams. -- `UrlParams.schemaJson` -> `UrlParams.schemaJsonField(field).pipe(Schema.decodeTo(schema), Schema.decodeEffect)`: Compose the field codec with the target schema, then decode it. +- `UrlParams.schemaJson` -> `Schema.JsonFromUrlParamsField(field).pipe(Schema.decodeTo(schema), Schema.decodeEffect)`: The field codec moved to effect/Schema. Compose it with the target schema, then decode it. -- `UrlParams.schemaParse` -> `UrlParamsFromString.pipe(Schema.decodeTo(UrlParams.schemaRecord.pipe(Schema.decodeTo(schema))))`: Recreate the removed helper by composing the string, record, and target codecs. +- `UrlParams.schemaParse` -> `UrlParamsFromString.pipe(Schema.decodeTo(Schema.RecordFromUrlParams.pipe(Schema.decodeTo(schema))))`: Recreate the removed helper by composing the string, record, and target codecs. -- `UrlParams.schemaRecord` -> `UrlParams.schemaRecord.pipe(Schema.decodeTo(schema))`: schemaRecord is now a base codec value; compose it with the target schema. +- `UrlParams.schemaRecord` -> `Schema.RecordFromUrlParams.pipe(Schema.decodeTo(schema))`: RecordFromUrlParams is a base codec in effect/Schema; compose it with the target schema. -- `UrlParams.schemaStruct` -> `UrlParams.schemaRecord.pipe(Schema.decodeTo(schema), Schema.decodeEffect)`: Compose the record codec with the target schema and decode it. +- `UrlParams.schemaStruct` -> `Schema.RecordFromUrlParams.pipe(Schema.decodeTo(schema), Schema.decodeEffect)`: Compose the record codec from effect/Schema with the target schema and decode it. - `UrlParams.toString` -> `UrlParams.toString`: Retained and broadened to accept any UrlParams.Input. @@ -7638,6 +7768,18 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `RpcSchema.isStreamSerializable` -> `RpcSchema.isStreamSchema(schema)`: The separate WithResult serializability predicate was removed; v4 RPC streaming is identified by its explicit Stream schema. +### `@effect/rpc/RpcSerialization` + +- `RpcSerialization.RpcSerializationError` -> `effect/unstable/rpc/RpcSerialization#MaxBufferSizeExceeded`: Buffer-limit failures now use MaxBufferSizeExceeded. MessagePack-specific decode errors have no counterpart. + +- `RpcSerialization.layerMsgPack` -> `effect/unstable/rpc/RpcSerialization#layerSchemaBinary`: MessagePack RPC serialization was removed. Use SchemaBinary, or layerNdjson when you need newline-delimited JSON framing. + +- `RpcSerialization.layerMsgPackWith` -> `effect/unstable/rpc/RpcSerialization#layerSchemaBinary`: MessagePack RPC serialization was removed. Pass maxFrameSize to layerSchemaBinary; NDJSON buffer limits remain on layerNdjsonWith. + +- `RpcSerialization.makeMsgPack` -> `effect/unstable/rpc/RpcSerialization#layerSchemaBinary`: MessagePack RPC serialization was removed. Construct SchemaBinary serialization with layerSchemaBinary. + +- `RpcSerialization.msgPack` -> `effect/unstable/rpc/RpcSerialization#layerSchemaBinary`: The MessagePack RpcSerialization service value was removed. Provide layerSchemaBinary instead. + ### `@effect/rpc/RpcServer` - `RpcServer.Protocol` -> `effect/unstable/rpc/RpcServer#Protocol`: Retained as a Context.Service; custom transports now expose a disconnect queue, explicit capability flags, and codecFor for schema-aware payload and exit encoding. @@ -7760,9 +7902,11 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `PgClient.PgClientConfig` -> `@effect/sql-pg/PgClient#PgClientConfig / PgPoolConfig`: Use PgClientConfig for base settings and PgPoolConfig for make/layer; pool sizing, idle timeout, and connection TTL moved to PgPoolConfig. -- `PgClient.PgClientFromPoolOptions` -> `Parameters[0]`: The named type was removed; derive the inline fromPool option type. PgPoolConfig is for creating a managed pool and is not equivalent. +- `PgClient.PgClientFromPoolOptions` -> `none`: The node-pg Pool wrapper options were removed with fromPool. Use PgClient.PgPoolConfig with PgClient.make or PgClient.layer. -- `PgClient.layerFromPool` -> `PgClient.layerFrom(PgClient.fromPool(options))`: Compose fromPool with layerFrom; layerFrom now accepts an Effect acquiring a PgClient rather than pool options. +- `PgClient.fromPool` -> `none`: Wrapping an existing node-pg Pool was removed with the native protocol client. Use PgClient.make or PgClient.layer with connection settings. + +- `PgClient.layerFromPool` -> `PgClient.layer`: Wrapping an existing node-pg Pool was removed. Provide connection settings to PgClient.layer instead. ### `@effect/sql-sqlite-bun/SqliteClient` @@ -7782,6 +7926,10 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `SqliteClient.asyncQuery` -> `@effect/sql-sqlite-react-native/SqliteClient#AsyncQuery`: Renamed and changed from FiberRef to Context.Reference; prefer withAsyncQuery or provide AsyncQuery as a service. +### `@effect/sql-sqlite-wasm/OpfsWorker` + +- `OpfsWorker.OpfsWorkerConfig`: TODO: needs guidance + ### `@effect/sql-sqlite-wasm/SqliteClient` - `SqliteClient.SqliteClient` -> `@effect/sql-sqlite-wasm/SqliteClient#SqliteClient`: Retained with the same export/import surface; the service value is now a Context.Service. @@ -7798,6 +7946,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Model.Class` -> `effect/unstable/schema/Model#Class`: Moved; model variants remain select, insert, update, json, jsonCreate, and jsonUpdate. +- `Model.Date` -> `effect/unstable/schema/Model#Date`: Moved; still serializes DateTime.Utc as a YYYY-MM-DD string. + - `Model.DateTimeFromDate` -> `effect/Schema#DateTimeUtcFromDate`: Moved to core Schema and retains Date to DateTime.Utc conversion. - `Model.Generated` -> `effect/unstable/schema/Model#GeneratedByDb`: Renamed and now read-only, with select and json variants only. Use Model.Field with select, update, and json to preserve writable v3 behavior. @@ -7808,7 +7958,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Model.fields` -> `effect/unstable/schema/Model#fields`: Moved with the variant-model helpers into core Effect's unstable schema package. -- `Model.makeDataLoaders` -> `effect/unstable/sql/SqlModel#makeResolvers`: Returns RequestResolvers instead of callable loaders; execute with SqlResolver.request and use RequestResolver delay/batch combinators for batching controls. +- `Model.makeDataLoaders` -> `effect/unstable/sql/SqlModel#makeResolvers`: Returns RequestResolvers instead of callable loaders; execute with SqlResolver.request and use RequestResolver delay/batch combinators for batching controls. The insert resolver requires model decoding services as well as insert-schema encoding services. ### `@effect/sql/SqlClient` @@ -7894,6 +8044,10 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Bounded.clamp` -> `Order.clamp(B.compare)`: Use the v4 Order combinator with { minimum: B.minBound, maximum: B.maxBound }; the Bounded dictionary itself was removed. +- `Bounded.max` -> `Reducer.make(Combiner.max(B.compare).combine, B.minBound)`: V4 removed Bounded dictionaries. Build the maximum Reducer from the separately retained Order and minimum bound. + +- `Bounded.min` -> `Reducer.make(Combiner.min(B.compare).combine, B.maxBound)`: V4 removed Bounded dictionaries. Build the minimum Reducer from the separately retained Order and maximum bound. + - `Bounded.reverse` -> `Order.flip(B.compare)`: Flip the Order and swap the separately stored minimum and maximum bounds; v4 has no bundled Bounded dictionary. ### `@effect/typeclass/Monoid` @@ -7904,6 +8058,10 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Monoid.fromSemigroup` -> `Reducer.make(S.combine, empty)`: Construct a v4 Reducer from the replacement Combiner operation and identity value. +- `Monoid.max` -> `Reducer.make(Combiner.max(B.compare).combine, B.minBound)`: Reducer replaces Monoid. Build it from the v4 maximum Combiner and the bounded order's minimum value. + +- `Monoid.min` -> `Reducer.make(Combiner.min(B.compare).combine, B.maxBound)`: Reducer replaces Monoid. Build it from the v4 minimum Combiner and the bounded order's maximum value. + - `Monoid.reverse` -> `Reducer.flip`: Use the v4 Reducer combinator; it preserves initialValue and reverses combine argument order. - `Monoid.struct` -> `Struct.makeReducer`: Pass a record of v4 Reducers to derive a field-wise Reducer. @@ -7936,6 +8094,10 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Semigroup.make` -> `Combiner.make`: Combiner replaces Semigroup. V4 accepts only the binary combine function and has no combineMany override. +- `Semigroup.max` -> `Combiner.max`: Combiner replaces Semigroup; pass the same Order to retain last-maximum tie behavior. + +- `Semigroup.min` -> `Combiner.min`: Combiner replaces Semigroup; pass the same Order to retain last-minimum tie behavior. + - `Semigroup.reverse` -> `Combiner.flip`: Use the v4 Combiner combinator to reverse combine argument order. - `Semigroup.struct` -> `Struct.makeCombiner`: Pass a record of v4 Combiners to derive a field-wise Combiner. @@ -8246,25 +8408,41 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/vitest/index` -- `index.ApiConfig` -> `vitest/node#ApiConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.ApiConfig` -> `vitest/node#ApiConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. - `index.ArgumentsType` -> `T extends (...args: infer A) => any ? A : never`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. - `index.Arrayable` -> `T | Array`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. +- `index.Assertion` -> `vitest#Assertion`: Vitest 5 takes the matcher return type first. Replace Assertion\ with Assertion\ or Assertion\, T\> for asynchronous assertions. + - `index.Awaitable` -> `T | PromiseLike`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. -- `index.BaseCoverageOptions` -> `vitest/node#BaseCoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.BaseCoverageOptions` -> `vitest/node#BaseCoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. + +- `index.BenchFactory` -> `vitest#Bench`: Use the Vitest 5 test-context bench fixture type. It is no longer the tinybench factory constructor. + +- `index.BenchFunction` -> `vitest#BenchFn`: Use BenchFn for the callback passed to the Vitest 5 test-context bench fixture. + +- `index.BenchTask` -> `vitest#BenchRegistration`: Migrate to a fixture registration and await its run() method; review its fields instead of treating it as a tinybench task. + +- `index.BenchTaskResult` -> `vitest#BenchResult`: Use the result returned by awaiting the Vitest 5 fixture registration's run() method. + +- `index.Benchmark` -> `vitest#TestBenchmark`: Use TestBenchmark for recorded benchmark data on a test; benchmarks are no longer standalone test tasks. -- `index.BenchmarkUserOptions` -> `vitest/node#BenchmarkUserOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.BenchmarkAPI` -> `vitest#Bench`: Use the test-context bench fixture. Move skip, only, and todo to the enclosing test. -- `index.BrowserConfigOptions` -> `vitest/node#BrowserConfigOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.BenchmarkResult` -> `vitest#BenchResult`: Use the result returned by awaiting the Vitest 5 fixture registration's run() method; review its changed fields. -- `index.BrowserScript` -> `vitest/node#BrowserScript`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.BenchmarkUserOptions` -> `vitest/node#BenchmarkUserOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.BuiltinEnvironment` -> `vitest/node#BuiltinEnvironment`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.BrowserConfigOptions` -> `vitest/node#BrowserConfigOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.CSSModuleScopeStrategy` -> `vitest/node#CSSModuleScopeStrategy`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.BrowserScript` -> `vitest/node#BrowserScript`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. + +- `index.BuiltinEnvironment` -> `vitest/node#BuiltinEnvironment`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. + +- `index.CSSModuleScopeStrategy` -> `vitest/node#CSSModuleScopeStrategy`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. - `index.CollectLineNumbers` -> `vitest/node#TypeCheckCollectLineNumbers`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. @@ -8274,97 +8452,101 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `index.Context` -> `vitest/node#TypeCheckContext`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. -- `index.CoverageIstanbulOptions` -> `vitest/node#CoverageIstanbulOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.CoverageIstanbulOptions` -> `vitest/node#CoverageIstanbulOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.CoverageOptions` -> `vitest/node#CoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.CoverageOptions` -> `vitest/node#CoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.CoverageProvider` -> `vitest/node#CoverageProvider`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.CoverageProvider` -> `vitest/node#CoverageProvider`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.CoverageProviderModule` -> `vitest/node#CoverageProviderModule`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.CoverageProviderModule` -> `vitest/node#CoverageProviderModule`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.CoverageReporter` -> `vitest/node#CoverageReporter`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.CoverageReporter` -> `vitest/node#CoverageReporter`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.CoverageV8Options` -> `vitest/node#CoverageV8Options`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.CoverageV8Options` -> `vitest/node#CoverageV8Options`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. - `index.Custom` -> `vitest#RunnerTestCase`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. -- `index.CustomProviderOptions` -> `vitest/node#CustomProviderOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.CustomProviderOptions` -> `vitest/node#CustomProviderOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.DepsOptimizationOptions` -> `vitest/node#DepsOptimizationOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.DepsOptimizationOptions` -> `vitest/node#DepsOptimizationOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. - `index.DoneCallback` -> `none`: Vitest does not support callback-style tests. Return a Promise or, in @effect/vitest tests, return an Effect. -- `index.Environment` -> `vitest/environments#Environment`: This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments. +- `index.Environment` -> `vitest/runtime#Environment`: This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime. -- `index.EnvironmentOptions` -> `vitest/node#EnvironmentOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.EnvironmentOptions` -> `vitest/node#EnvironmentOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.EnvironmentReturn` -> `vitest/environments#EnvironmentReturn`: This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments. +- `index.EnvironmentReturn` -> `vitest/runtime#EnvironmentReturn`: This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime. - `index.ErrorWithDiff` -> `vitest#TestError`: Vitest 3 deprecated ErrorWithDiff in favor of TestError; review the tightened actual, expected, and cause fields. -- `index.ExtendedContext` -> `vitest#TestContext`: The separate context alias was removed. Vitest 4 uses TestContext, which includes the current task and lifecycle methods. +- `index.ExpectPollOptions` -> `NonNullable[1]>`: Vitest 5 removes the named options export; derive the options from the public expect.poll function. + +- `index.ExtendedContext` -> `vitest#TestContext`: The separate context alias was removed. Vitest 5 uses TestContext, which includes the current task and lifecycle methods. - `index.File` -> `vitest#RunnerTestFile`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. -- `index.HappyDOMOptions` -> `NonNullable`: Vitest 4 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type. +- `index.HappyDOMOptions` -> `NonNullable`: Vitest 5 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type. -- `index.HookCleanupCallback` -> `none`: No named Vitest 4 export replaces this alias. Let the hook return type infer, or type the cleanup function locally. +- `index.HookCleanupCallback` -> `none`: No named Vitest 5 export replaces this alias. Let the hook return type infer, or type the cleanup function locally. -- `index.HookListener` -> `none`: Use the matching @vitest/runner hook-specific type such as BeforeAllListener, AfterAllListener, BeforeEachListener, or AfterEachListener for custom runner code. +- `index.HookListener` -> `none`: Infer the callback from the public hook function, or derive it with Parameters\[0] and the corresponding hook name. -- `index.InlineConfig` -> `vitest/node#InlineConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.InlineConfig` -> `vitest/node#InlineConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.JSDOMOptions` -> `NonNullable`: Vitest 4 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type. +- `index.JSDOMOptions` -> `NonNullable`: Vitest 5 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type. + +- `index.Matchers` -> `vitest#Matchers`: Augment vitest.Matchers\ for custom matchers. R is the matcher return type and T is the received value; @vitest/expect no longer shares Vitest's assertion state. - `index.Mock` -> `vitest#Mock`: This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route. -- `index.ModuleCache` -> `none`: Vitest 3 marked this unused internal cache shape deprecated; Vitest 4 has no public replacement. +- `index.ModuleCache` -> `none`: Vitest 3 marked this unused internal cache shape deprecated; Vitest 5 has no public replacement. - `index.MutableArray` -> `{ -readonly [K in keyof T]: T[K] }`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. - `index.Nullable` -> `T | null | undefined`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. -- `index.Pool` -> `vitest/node#Pool`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.Pool` -> `vitest/node#Pool`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.PoolOptions` -> `vitest/config#TestUserConfig`: The v3 built-in poolOptions object was removed. Move its fields to Vitest 4 top-level config such as maxWorkers and vmMemoryLimit; vitest/node PoolOptions is a different custom-pool API. +- `index.PoolOptions` -> `vitest/config#TestUserConfig`: The v3 built-in poolOptions object was removed. Move its fields to Vitest 5 top-level config such as maxWorkers and vmMemoryLimit; vitest/node PoolOptions is a different custom-pool API. -- `index.ProjectConfig` -> `vitest/node#ProjectConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.ProjectConfig` -> `vitest/node#ProjectConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. - `index.RawErrsMap` -> `vitest/node#TypeCheckRawErrorsMap`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. -- `index.ReportContext` -> `vitest/node#ReportContext`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.ReportContext` -> `vitest/node#ReportContext`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.Reporter` -> `vitest/reporters#Reporter`: Import Reporter from the public plural vitest/reporters entrypoint; its lifecycle methods changed in Vitest 4. +- `index.Reporter` -> `vitest/node#Reporter`: Import Reporter from vitest/node; the deprecated vitest/reporters entrypoint was removed in Vitest 5. - `index.ResolveIdFunction` -> `none`: This deprecated vite-node callback was removed. Use Vite environment or module-runner APIs. -- `index.ResolvedConfig` -> `vitest/node#ResolvedConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.ResolvedConfig` -> `vitest/node#ResolvedConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.ResolvedCoverageOptions` -> `vitest/node#ResolvedCoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.ResolvedCoverageOptions` -> `vitest/node#ResolvedCoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.ResolvedTestEnvironment` -> `none`: Vitest 3 marked this type unsupported. Use Environment from vitest/environments for custom environments. +- `index.ResolvedTestEnvironment` -> `none`: Vitest 3 marked this type unsupported. Use Environment from vitest/runtime for custom environments. - `index.RootAndTarget` -> `vitest/node#TypeCheckRootAndTarget`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. - `index.RunnerCustomCase` -> `vitest#RunnerTestCase`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. -- `index.RuntimeContext` -> `@vitest/runner#RuntimeContext`: Custom-runner code can add an explicit @vitest/runner dependency; ordinary tests should avoid this internal state type. +- `index.RuntimeContext` -> `none`: Vitest 5 deprecates @vitest/runner and does not expose this internal state type. Extend TestRunner from vitest and use its public methods instead. -- `index.SequenceHooks` -> `vitest/node#SequenceHooks`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.SequenceHooks` -> `vitest/node#SequenceHooks`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.SequenceSetupFiles` -> `vitest/node#SequenceSetupFiles`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.SequenceSetupFiles` -> `vitest/node#SequenceSetupFiles`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. - `index.SerializableSpec` -> `vitest#SerializedTestSpecification`: Use the non-deprecated Vitest name; SerializableSpec was only an alias. - `index.Suite` -> `vitest#RunnerTestSuite`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. -- `index.SuiteHooks` -> `@vitest/runner#SuiteHooks`: Custom-runner code can add an explicit @vitest/runner dependency; ordinary tests should use public hook functions. +- `index.SuiteHooks` -> `ReturnType`: Derive the hook collection from Vitest 5's public TestRunner API; ordinary tests should use public hook functions. - `index.Task` -> `vitest#RunnerTask`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. - `index.TaskBase` -> `vitest#RunnerTaskBase`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. -- `index.TaskContext` -> `vitest#TestContext`: The separate context alias was removed. Vitest 4 uses TestContext, which includes the current task and lifecycle methods. +- `index.TaskContext` -> `vitest#TestContext`: The separate context alias was removed. Vitest 5 uses TestContext, which includes the current task and lifecycle methods. - `index.TaskResult` -> `vitest#RunnerTaskResult`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. @@ -8376,32 +8558,40 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `index.TscErrorInfo` -> `vitest/node#TypeCheckErrorInfo`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. -- `index.TypecheckConfig` -> `vitest/node#TypecheckConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.TypecheckConfig` -> `vitest/node#TypecheckConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.UserConfig` -> `vitest/config#TestUserConfig`: Vitest 4 exposes its config as TestUserConfig; ViteUserConfig is the separate Vite configuration type. +- `index.UserConfig` -> `vitest/config#TestUserConfig`: Vitest 5 exposes its config as TestUserConfig; ViteUserConfig is the separate Vite configuration type. - `index.UserWorkspaceConfig` -> `vitest/config#UserWorkspaceConfig`: Import the type from vitest/config and migrate Vitest workspace configuration to projects. -- `index.VitestEnvironment` -> `vitest/node#VitestEnvironment`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.VitestEnvironment` -> `vitest/node#VitestEnvironment`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.VitestRunMode` -> `vitest/node#VitestRunMode`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.VitestRunMode` -> `vitest/node#VitestRunMode`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.VmEnvironmentReturn` -> `vitest/environments#VmEnvironmentReturn`: This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments. +- `index.VmEnvironmentReturn` -> `vitest/runtime#VmEnvironmentReturn`: This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime. -- `index.WorkerContext` -> `vitest/node#WorkerContext`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. +- `index.WorkerContext` -> `vitest/node#WorkerContext`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. - `index.WorkerRPC` -> `none`: The concrete worker RPC composition is internal. Use public Vitest RuntimeRPC, RunnerRPC, ContextRPC, or WorkerRequest types only when their narrower contract fits. +- `index.bench` -> `vitest#test`: Vitest 5 removes the top-level bench export. Destructure bench from a regular test's context and await bench(name, fn).run(); use skip, only, or todo on the enclosing test. + - `index.chai.Should` -> `vitest#chai.Should`: This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route. +- `index.describe` -> `vitest#describe`: Vitest 5 removes describe.sequential and sequential options. Use describe(name, { concurrent: false }, body) for suites that depend on ordering. + - `index.expect` -> `vitest#expect`: This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route. +- `index.it` -> `@effect/vitest#it`: Effect helpers retain their calling convention. Vitest 5 removes it.sequential; pass { concurrent: false } to the native test or as the Effect helper's third argument. + - `index.scoped` -> `@effect/vitest#effect`: V4 effect tests are scoped and provide the test environment. Replace scoped(...) with effect(...), and it.scoped(...) with it.effect(...). - `index.scopedLive` -> `@effect/vitest#live`: V4 live tests are scoped automatically. Replace scopedLive(...) with live(...), and it.scopedLive(...) with it.live(...). - `index.should` -> `vitest#should`: This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route. +- `index.test` -> `vitest#test`: Vitest 5 removes test.sequential and sequential options. Pass { concurrent: false } to opt out of inherited concurrency. + ### `@effect/vitest/utils` - `utils.assertFailure` -> `assertExitFailure`: For v3 Exit values, rename to assertExitFailure. In v4, assertFailure instead asserts Result.Failure. @@ -8446,7 +8636,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `DurableDeferred.failCause` -> `effect/unstable/workflow/DurableDeferred#failCause`: Moved into core Effect and now requires the error schema encoding services. -- `DurableDeferred.into` -> `effect/unstable/workflow/DurableDeferred#into`: Moved into core Effect with the same exit recording and suspension propagation behavior. +- `DurableDeferred.into` -> `effect/unstable/workflow/DurableDeferred#into`: Moved into core Effect with the same exit recording and suspension propagation behavior. Provide both decoding and encoding services for the success and error schemas; recording the exit requires encoding services. - `DurableDeferred.make` -> `effect/unstable/workflow/DurableDeferred#make`: Moved into core Effect with the same name and optional schemas, expressed through v4 Schema.Constraint. @@ -8468,6 +8658,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Workflow.CaptureDefects` -> `effect/unstable/workflow/Workflow#CaptureDefects`: Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value with the same true default. +- `Workflow.Complete`: TODO: needs guidance + - `Workflow.Execution` -> `effect/unstable/workflow/Workflow#Execution`: Moved into core Effect; its workflow discriminator changed from name to \_tag. - `Workflow.Requirements` -> `Workflow.RequirementsClient / Workflow.RequirementsHandler`: The schema Context union split by direction: client payload encoding and result decoding versus handler payload decoding and result encoding. @@ -8502,6 +8694,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `WorkflowProxy.ConvertHttpApi` -> `effect/unstable/workflow/WorkflowProxy#ConvertHttpApi`: Moved into core Effect and updated to v4 HttpApiEndpoint types and the consolidated HttpApi architecture. +- `WorkflowProxy.ConvertRpcs` -> `effect/unstable/workflow/WorkflowProxy#ConvertRpcs`: Moved into core Effect; generated execute, discard, and resume RPCs are now keyed from workflow \_tag. + ### `@effect/workflow/WorkflowProxyServer` - `WorkflowProxyServer.layerHttpApi` -> `effect/unstable/workflow/WorkflowProxyServer#layerHttpApi`: Moved into core Effect. Use v4 HttpApi group identifiers and Workflow.RequirementsHandler schema services. @@ -8510,34 +8704,34 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `effect/Arbitrary` -- `Arbitrary.ArbitraryAnnotation` -> `Schema.Annotations.ToArbitrary.Declaration`: Arbitrary derivation annotations now live in Schema.Annotations and use the toArbitrary key. +- `Arbitrary.ArbitraryAnnotation` -> `Schema.Annotations.ToArbitrary.Declaration`: Arbitrary derivation annotations now live in Schema.Annotations. Attach a toCodecArbitrary declaration callback that returns a Schema Link. -- `Arbitrary.ArbitraryGenerationContext` -> `Schema.Annotations.ToArbitrary.Context`: Use the v4 arbitrary-derivation context type from Schema.Annotations. +- `Arbitrary.ArbitraryGenerationContext` -> `Schema.Annotations.ToArbitrary.DeclarationInput`: Native arbitrary callbacks receive DeclarationInput with decoded type-parameter schemas and normalized constraints. -- `Arbitrary.LazyArbitrary` -> `Schema.Arbitrary`: The arbitrary factory type moved onto Schema. +- `Arbitrary.LazyArbitrary` -> `effect/unstable/arbitrary/Arbitrary#Arbitrary`: The generated-value description is now the native Arbitrary interface from effect/unstable/arbitrary. #### `Arbitrary.make` -**Replacement:** `Schema.toArbitrary` +**Replacement:** `effect/unstable/arbitrary/Arbitrary#schema` -Arbitrary derivation is now exposed directly by Schema. +Derive a native Arbitrary from a Schema. Effect no longer bridges to fast-check. **Example** ```ts -Schema.toArbitrary(schema)(FastCheck) +Arbitrary.schema(schema) ``` #### `Arbitrary.makeLazy` -**Replacement:** `Schema.toArbitrary` +**Replacement:** `effect/unstable/arbitrary/Arbitrary#schema` -Lazy arbitrary derivation is now exposed directly by Schema. +Lazy and eager Schema derivation are the same native Arbitrary.schema constructor. **Example** ```ts -Schema.toArbitrary(schema) +Arbitrary.schema(schema) ``` ### `effect/Array` @@ -8946,9 +9140,9 @@ Schema.toArbitrary(schema) - `Channel.repeated` -> `Channel.forever`: Use forever for infinite repetition. Channel.repeat takes a Schedule and may terminate, so it is not equivalent. -- `Channel.run` -> `Channel.runDone`: Renamed to runDone for an inputless, outputless channel. Use runDrain if emitted elements should be discarded. +- `Channel.run` -> `Channel.runDrain`: Use runDrain to consume all emitted elements and return the channel's done value. -- `Channel.runScoped` -> `Channel.toPull`: No direct scoped runner remains. Use toPull in the caller scope and recover Cause.Done; use runDone or runDrain when an internally managed scope is acceptable. +- `Channel.runScoped` -> `Channel.toPull`: No direct scoped runner remains. Use toPull in the caller scope and recover Cause.Done; use runDrain when an internally managed scope is acceptable. - `Channel.scopedWith` -> `Channel.unwrap`: Use Channel.unwrap(Effect.map(Effect.scope, (scope) =\> Channel.fromEffect(f(scope)))) so the effect uses the active channel scope. @@ -9032,17 +9226,17 @@ Schema.toArbitrary(schema) - `Config.all` -> `Config.all`: Combine an iterable or record of Config values. A wholly absent product can use Config.withDefault or Config.option, while a partially supplied product fails. -- `Config.array` -> `Config.schema(Config.Array(valueSchema), path)`: Array parsing is schema-based in v4; rebuild the element Config as a Schema and pass the optional path to Config.schema. +- `Config.array` -> `Config.Array(valueSchema, path)`: Array parsing is schema-based in v4; rebuild the element Config as a Schema and pass it with the optional path to Config.Array. -- `Config.boolean` -> `Config.boolean`: Unchanged. +- `Config.boolean` -> `Config.Boolean`: Direct constructor rename. - `Config.branded` -> `Config.schema(schema.pipe(Schema.brand(brand)), path)`: Brand validation moved to Schema; define the branded schema and construct the Config with Config.schema. - `Config.chunk` -> `Config.schema(Schema.Chunk(valueSchema), path)`: Collection parsing is schema-based in v4; use Schema.Chunk when a Chunk result is still required. -- `Config.date` -> `Config.date`: Unchanged. +- `Config.date` -> `Config.Date`: Direct constructor rename. -- `Config.duration` -> `Config.duration`: Unchanged. +- `Config.duration` -> `Config.Duration`: Direct constructor rename. - `Config.fail` -> `Config.fail`: The v4 constructor takes a ConfigProvider.SourceError or Schema.SchemaError instead of a message; wrap the failure in the appropriate cause. @@ -9050,31 +9244,33 @@ Schema.toArbitrary(schema) - `Config.hashSet` -> `Config.schema(Schema.HashSet(valueSchema), path)`: HashSet parsing is schema-based in v4; replace the child Config with its value Schema. -- `Config.integer` -> `Config.int`: Renamed to the shorter v4 integer constructor. +- `Config.integer` -> `Config.Int`: Renamed to the shorter v4 integer constructor using the PascalCase constructor convention. + +- `Config.literal` -> `Config.Literals(literals, path)`: The v3 curried variadic constructor became Config.Literals with an array and inline path; use Config.Literal for one value. -- `Config.literal` -> `Config.literals(literals, path)`: The v3 curried variadic constructor became Config.literals with an array and inline path; use Config.literal for one value. +- `Config.logLevel` -> `Config.LogLevel`: Direct constructor rename. -- `Config.logLevel` -> `Config.logLevel`: Unchanged. +- `Config.mapAttempt` -> `Config.mapEffect`: Catch exceptions explicitly and return an Effect failure containing Config.ConfigError; mapEffect is Effect-based in v4. -- `Config.mapAttempt` -> `Config.mapOrFail`: Catch exceptions explicitly and return an Effect failure containing Config.ConfigError; mapOrFail is Effect-based in v4. +- `Config.mapOrFail` -> `Config.mapEffect`: Renamed to match the effectful mapping convention used throughout the library. -- `Config.nonEmptyString` -> `Config.nonEmptyString`: Unchanged. +- `Config.nonEmptyString` -> `Config.NonEmptyString`: Direct constructor rename. -- `Config.number` -> `Config.number`: Unchanged; use Config.finite when NaN and infinities must be rejected. +- `Config.number` -> `Config.Number`: Direct constructor rename; use Config.Finite when NaN and infinities must be rejected. - `Config.orElseIf` -> `Config.orElse`: The fallback now receives Config.ConfigError; test it in the callback and re-fail with Config.fail(error.cause) when the predicate is false. -- `Config.port` -> `Config.port`: Unchanged. +- `Config.port` -> `Config.Port`: Direct constructor rename. - `Config.primitive` -> `Config.schema(customSchema, path)`: Custom primitive parsing moved to Schema codecs; express decoding and diagnostics in a Schema, then pass it to Config.schema. Its canonical StringTree encoding must expose a concrete shape; opaque encodings such as Schema.Any or Schema.Unknown are not supported. -- `Config.redacted` -> `Config.redacted`: The string/path overload remains; replace the v3 Config argument overload with Config.map(config, Redacted.make). +- `Config.redacted` -> `Config.Redacted`: The string/path overload remains; replace the v3 Config argument overload with Config.map(config, Redacted.make). -- `Config.repeat` -> `Config.schema(Config.Array(valueSchema), path)`: Repeated values are represented by an array Schema in v4; Config.Array also accepts flat separated input. +- `Config.repeat` -> `Config.Array(valueSchema, path)`: Repeated values use the Config.Array constructor, which accepts structural arrays and flat separated input. -- `Config.secret` -> `Config.redacted`: Secret was removed in favor of Redacted; this constructor already returns Redacted\. +- `Config.secret` -> `Config.Redacted`: Secret was removed in favor of Redacted; this constructor returns Redacted\. -- `Config.string` -> `Config.string`: Unchanged. +- `Config.string` -> `Config.String`: Direct constructor rename. - `Config.succeed` -> `Config.succeed`: Unchanged. @@ -9082,7 +9278,7 @@ Schema.toArbitrary(schema) - `Config.sync` -> `Config.succeed(undefined).pipe(Config.map(() => thunk()))`: The dedicated lazy constant constructor was removed; mapping a constant Config preserves evaluation at parse time. -- `Config.url` -> `Config.url`: Unchanged. +- `Config.url` -> `Config.URL`: Direct constructor rename. - `Config.validate` -> `Config.schema(schema.check(check), path)`: Validation moved to Schema checks; attach the predicate and message to the Schema used by Config.schema. @@ -9144,7 +9340,7 @@ Schema.toArbitrary(schema) - `ConfigProvider.ConfigProvider.Flat` -> `ConfigProvider.ConfigProvider`: Flat providers were removed; implement the unified path-based provider with ConfigProvider.make. -- `ConfigProvider.ConfigProvider.FromEnvConfig` -> `Parameters[0]`: Options are inline in v4 and contain env plus preserveEmptyStrings; custom path and sequence delimiters moved to provider path transforms and Config.Array/Config.Record schemas. +- `ConfigProvider.ConfigProvider.FromEnvConfig` -> `Parameters[0]`: Options are inline in v4 and contain env plus preserveEmptyStrings; custom path delimiters moved to provider path transforms, while separated sequences and records use Config.Array and Config.Record. - `ConfigProvider.ConfigProvider.FromMapConfig` -> `none`: fromMap and its delimiter options were removed; expand delimited keys into a nested value and use ConfigProvider.fromUnknown. @@ -9846,7 +10042,7 @@ Schema.toArbitrary(schema) - `Effect.transposeMapOption` -> `Option.match`: Return `Effect.succeedNone` for None and map the Effect result to Some. Adapt arguments and imports to the v4 API. -- `Effect.try` -> `Effect.try`: Still exported in v4; update call sites for the revised signature, options, and channel inference. +- `Effect.try` -> `Effect.try`: Use the callback overload for Cause.UnknownError, or the object overload with try and catch to map failures to a custom error. The callback-only overload does not accept a custom error type parameter. - `Effect.tryMap` -> `Effect.flatMap + Effect.try`: FlatMap the source value into the v4 synchronous try constructor. Adapt arguments and imports to the v4 API. @@ -9936,7 +10132,7 @@ Schema.toArbitrary(schema) - `Effectable.ChannelTypeId` -> `Channel.TypeId`: The public channel brand moved to its owning module; v4 uses a string TypeId rather than the v3 Symbol. -- `Effectable.Class` -> `Effectable.Class`: Still available; replace commit() with an override property or getter returning the Effect. +- `Effectable.Class` -> `Effectable.Class`: Still available; replace commit() with an asEffect() method returning the Effect. The intermediate v4 override property/getter is no longer supported. - `Effectable.CommitPrimitive` -> `new() => Effect.Effect`: The named constructor interface was removed; inline the constructor type when needed. @@ -9950,7 +10146,7 @@ Schema.toArbitrary(schema) - `Effectable.StreamTypeId` -> `Stream.TypeId`: The public stream brand moved to its owning module; v4 uses a string TypeId rather than the v3 Symbol. -- `Effectable.StructuralClass` -> `Effectable.Class`: Use Class and migrate commit() to override; v4 equality is structural by default. +- `Effectable.StructuralClass` -> `Effectable.Class`: Use Class and migrate commit() to asEffect(); v4 equality is structural by default. - `Effectable.StructuralCommitPrototype` -> `Effectable.Prototype`: Use Prototype with evaluate; a separate structural prototype is unnecessary because v4 equality is structural by default. @@ -9986,6 +10182,8 @@ Schema.toArbitrary(schema) - `Either.filterOrLeft` -> `Result.filterOrFail`: Left is now Failure, so the predicate combinator is filterOrFail. +- `Either.flatMap`: TODO: needs guidance + - `Either.flip` -> `Result.flip`: The channel-swapping combinator moved to Result. - `Either.fromNullable` -> `Result.fromNullishOr`: Renamed with v4 nullish-or terminology. @@ -10130,6 +10328,8 @@ Schema.toArbitrary(schema) - `Exit.exists` -> `Exit.isSuccess`: No direct v4 combinator; use Exit.isSuccess(self) && predicate(self.value). If callers rely on the refinement overload, retain an explicitly typed wrapper returning self is Exit.Exit\. +- `Exit.flatMap`: TODO: needs guidance + - `Exit.flatMapEffect` -> `Effect.matchCauseEffectEager`: Use Effect.matchCauseEffectEager(self, { onFailure: cause =\> Effect.succeed(Exit.failCause(cause)), onSuccess: f }). The explicit failure branch is required because v3 preserved an input Failure as a successful outer Effect; plain Effect.flatMap would instead fail the outer Effect. - `Exit.flatten` -> `Exit.match`: No direct v4 Exit flatten; use Exit.match(self, { onFailure: Exit.failCause, onSuccess: identity }) to return the inner Exit on success and preserve an outer failure as Exit data. @@ -10162,15 +10362,219 @@ Schema.toArbitrary(schema) ### `effect/FastCheck` -- `FastCheck.BigUintConstraints` -> `FastCheck.BigIntConstraints`: Import FastCheck from effect/testing. Unsigned bigint constraints were consolidated into BigIntConstraints with min: 0n. +- `FastCheck.Arbitrary`: TODO: needs guidance + +- `FastCheck.ArrayConstraints`: TODO: needs guidance + +- `FastCheck.AsyncCommand`: TODO: needs guidance + +- `FastCheck.AsyncPropertyHookFunction`: TODO: needs guidance + +- `FastCheck.BigIntArrayConstraints`: TODO: needs guidance + +- `FastCheck.BigIntConstraints`: TODO: needs guidance + +- `FastCheck.BigUintConstraints` -> `FastCheck.BigIntConstraints`: Depend on fast-check and import it directly. Unsigned bigint constraints were consolidated into BigIntConstraints with min: 0n. + +- `FastCheck.CloneValue`: TODO: needs guidance + +- `FastCheck.Command`: TODO: needs guidance + +- `FastCheck.CommandsContraints`: TODO: needs guidance + +- `FastCheck.ContextValue`: TODO: needs guidance + +- `FastCheck.DateConstraints`: TODO: needs guidance + +- `FastCheck.DepthContext`: TODO: needs guidance + +- `FastCheck.DepthIdentifier`: TODO: needs guidance + +- `FastCheck.DepthSize`: TODO: needs guidance + +- `FastCheck.DictionaryConstraints`: TODO: needs guidance + +- `FastCheck.DomainConstraints`: TODO: needs guidance + +- `FastCheck.DoubleConstraints`: TODO: needs guidance + +- `FastCheck.EmailAddressConstraints`: TODO: needs guidance + +- `FastCheck.ExecutionStatus`: TODO: needs guidance + +- `FastCheck.ExecutionTree`: TODO: needs guidance + +- `FastCheck.FalsyContraints`: TODO: needs guidance + +- `FastCheck.FalsyValue`: TODO: needs guidance + +- `FastCheck.Float32ArrayConstraints`: TODO: needs guidance + +- `FastCheck.Float64ArrayConstraints`: TODO: needs guidance + +- `FastCheck.FloatConstraints`: TODO: needs guidance + +- `FastCheck.GeneratorValue`: TODO: needs guidance + +- `FastCheck.GlobalAsyncPropertyHookFunction`: TODO: needs guidance + +- `FastCheck.GlobalParameters`: TODO: needs guidance + +- `FastCheck.GlobalPropertyHookFunction`: TODO: needs guidance + +- `FastCheck.IAsyncProperty`: TODO: needs guidance + +- `FastCheck.IAsyncPropertyWithHooks`: TODO: needs guidance + +- `FastCheck.ICommand`: TODO: needs guidance + +- `FastCheck.IProperty`: TODO: needs guidance + +- `FastCheck.IPropertyWithHooks`: TODO: needs guidance + +- `FastCheck.IRawProperty`: TODO: needs guidance + +- `FastCheck.IntArrayConstraints`: TODO: needs guidance + +- `FastCheck.IntegerConstraints`: TODO: needs guidance + +- `FastCheck.JsonSharedConstraints`: TODO: needs guidance + +- `FastCheck.LetrecLooselyTypedBuilder`: TODO: needs guidance + +- `FastCheck.LetrecLooselyTypedTie`: TODO: needs guidance + +- `FastCheck.LetrecTypedBuilder`: TODO: needs guidance + +- `FastCheck.LetrecTypedTie`: TODO: needs guidance + +- `FastCheck.LetrecValue`: TODO: needs guidance + +- `FastCheck.LoremConstraints`: TODO: needs guidance + +- `FastCheck.MaybeWeightedArbitrary`: TODO: needs guidance + +- `FastCheck.Memo`: TODO: needs guidance + +- `FastCheck.MixedCaseConstraints`: TODO: needs guidance + +- `FastCheck.ModelRunAsyncSetup`: TODO: needs guidance + +- `FastCheck.ModelRunSetup`: TODO: needs guidance + +- `FastCheck.NatConstraints`: TODO: needs guidance + +- `FastCheck.ObjectConstraints`: TODO: needs guidance + +- `FastCheck.OneOfConstraints`: TODO: needs guidance + +- `FastCheck.OneOfValue`: TODO: needs guidance -- `FastCheck.UnicodeJsonSharedConstraints` -> `FastCheck.JsonSharedConstraints`: Import FastCheck from effect/testing. Unicode JSON generation was consolidated into JsonSharedConstraints.stringUnit. +- `FastCheck.OptionConstraints`: TODO: needs guidance + +- `FastCheck.Parameters`: TODO: needs guidance + +- `FastCheck.PreconditionFailure`: TODO: needs guidance + +- `FastCheck.PropertyHookFunction`: TODO: needs guidance + +- `FastCheck.Random`: TODO: needs guidance + +- `FastCheck.RandomType`: TODO: needs guidance + +- `FastCheck.RecordConstraints`: TODO: needs guidance + +- `FastCheck.RecordValue`: TODO: needs guidance + +- `FastCheck.RunDetails`: TODO: needs guidance + +- `FastCheck.RunDetailsCommon`: TODO: needs guidance + +- `FastCheck.RunDetailsFailureInterrupted`: TODO: needs guidance + +- `FastCheck.RunDetailsFailureProperty`: TODO: needs guidance + +- `FastCheck.RunDetailsFailureTooManySkips`: TODO: needs guidance + +- `FastCheck.RunDetailsSuccess`: TODO: needs guidance + +- `FastCheck.SchedulerAct`: TODO: needs guidance + +- `FastCheck.SchedulerConstraints`: TODO: needs guidance + +- `FastCheck.SchedulerReportItem`: TODO: needs guidance + +- `FastCheck.SchedulerSequenceItem`: TODO: needs guidance + +- `FastCheck.ShuffledSubarrayConstraints`: TODO: needs guidance + +- `FastCheck.Size`: TODO: needs guidance + +- `FastCheck.SizeForArbitrary`: TODO: needs guidance + +- `FastCheck.SparseArrayConstraints`: TODO: needs guidance + +- `FastCheck.StringConstraints`: TODO: needs guidance + +- `FastCheck.StringMatchingConstraints`: TODO: needs guidance + +- `FastCheck.StringSharedConstraints`: TODO: needs guidance + +- `FastCheck.SubarrayConstraints`: TODO: needs guidance + +- `FastCheck.UnicodeJsonSharedConstraints` -> `FastCheck.JsonSharedConstraints`: Depend on fast-check and import it directly. Unicode JSON generation was consolidated into JsonSharedConstraints.stringUnit. + +- `FastCheck.UniqueArrayConstraints`: TODO: needs guidance + +- `FastCheck.UniqueArrayConstraintsCustomCompare`: TODO: needs guidance + +- `FastCheck.UniqueArrayConstraintsCustomCompareSelect`: TODO: needs guidance + +- `FastCheck.UniqueArrayConstraintsRecommended`: TODO: needs guidance + +- `FastCheck.UniqueArraySharedConstraints`: TODO: needs guidance + +- `FastCheck.UuidConstraints`: TODO: needs guidance + +- `FastCheck.Value`: TODO: needs guidance + +- `FastCheck.VerbosityLevel`: TODO: needs guidance + +- `FastCheck.WebAuthorityConstraints`: TODO: needs guidance + +- `FastCheck.WebFragmentsConstraints`: TODO: needs guidance + +- `FastCheck.WebPathConstraints`: TODO: needs guidance + +- `FastCheck.WebQueryParametersConstraints`: TODO: needs guidance + +- `FastCheck.WebSegmentConstraints`: TODO: needs guidance + +- `FastCheck.WebUrlConstraints`: TODO: needs guidance + +- `FastCheck.WeightedArbitrary`: TODO: needs guidance + +- `FastCheck.WithAsyncToStringMethod`: TODO: needs guidance + +- `FastCheck.WithCloneMethod`: TODO: needs guidance + +- `FastCheck.WithToStringMethod`: TODO: needs guidance + +- `FastCheck.__commitHash`: TODO: needs guidance + +- `FastCheck.__type`: TODO: needs guidance + +- `FastCheck.__version`: TODO: needs guidance + +- `FastCheck.anything`: TODO: needs guidance + +- `FastCheck.array`: TODO: needs guidance #### `FastCheck.ascii` **Replacement:** `FastCheck.string` -Import FastCheck from effect/testing. fast-check v4 replaced character arbitraries with string units. +Depend on fast-check and import it directly. fast-check v4 replaced character arbitraries with string units. **Example** @@ -10182,7 +10586,7 @@ FastCheck.string({ unit: "binary-ascii", minLength: 1, maxLength: 1 }) **Replacement:** `FastCheck.string` -Import FastCheck from effect/testing. Use the binary-ascii string unit. +Depend on fast-check and import it directly. Use the binary-ascii string unit. **Example** @@ -10190,11 +10594,23 @@ Import FastCheck from effect/testing. Use the binary-ascii string unit. FastCheck.string({ ...constraints, unit: "binary-ascii" }) ``` +- `FastCheck.assert`: TODO: needs guidance + +- `FastCheck.asyncDefaultReportMessage`: TODO: needs guidance + +- `FastCheck.asyncModelRun`: TODO: needs guidance + +- `FastCheck.asyncProperty`: TODO: needs guidance + +- `FastCheck.asyncStringify`: TODO: needs guidance + +- `FastCheck.asyncToStringMethod`: TODO: needs guidance + #### `FastCheck.base64` **Replacement:** `FastCheck.constantFrom` -Import FastCheck from effect/testing. Generate one base64 alphabet character; base64String remains for complete encoded strings. +Depend on fast-check and import it directly. Generate one base64 alphabet character; base64String remains for complete encoded strings. **Example** @@ -10202,13 +10618,19 @@ Import FastCheck from effect/testing. Generate one base64 alphabet character; ba FastCheck.constantFrom(..."abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/") ``` -- `FastCheck.bigIntN` -> `FastCheck.bigInt`: Import FastCheck from effect/testing. Express the signed bit range with min and max constraints. +- `FastCheck.base64String`: TODO: needs guidance + +- `FastCheck.bigInt`: TODO: needs guidance + +- `FastCheck.bigInt64Array`: TODO: needs guidance + +- `FastCheck.bigIntN` -> `FastCheck.bigInt`: Depend on fast-check and import it directly. Express the signed bit range with min and max constraints. #### `FastCheck.bigUint` **Replacement:** `FastCheck.bigInt` -Import FastCheck from effect/testing. Use a minimum of 0n and the previous maximum. +Depend on fast-check and import it directly. Use a minimum of 0n and the previous maximum. **Example** @@ -10216,11 +10638,13 @@ Import FastCheck from effect/testing. Use a minimum of 0n and the previous maxim FastCheck.bigInt({ min: 0n, max }) ``` +- `FastCheck.bigUint64Array`: TODO: needs guidance + #### `FastCheck.bigUintN` **Replacement:** `FastCheck.bigInt` -Import FastCheck from effect/testing. Express the unsigned bit range with min and max constraints. +Depend on fast-check and import it directly. Express the unsigned bit range with min and max constraints. **Example** @@ -10228,11 +10652,13 @@ Import FastCheck from effect/testing. Express the unsigned bit range with min an FastCheck.bigInt({ min: 0n, max: (1n << BigInt(n)) - 1n }) ``` +- `FastCheck.boolean`: TODO: needs guidance + #### `FastCheck.char` **Replacement:** `FastCheck.string` -Import FastCheck from effect/testing. Use a one-unit printable ASCII string. +Depend on fast-check and import it directly. Use a one-unit printable ASCII string. **Example** @@ -10244,7 +10670,7 @@ FastCheck.string({ unit: "grapheme-ascii", minLength: 1, maxLength: 1 }) **Replacement:** `FastCheck.nat` -Import FastCheck from effect/testing. Map a 16-bit natural number through String.fromCharCode. +Depend on fast-check and import it directly. Map a 16-bit natural number through String.fromCharCode. **Example** @@ -10252,15 +10678,55 @@ Import FastCheck from effect/testing. Map a 16-bit natural number through String FastCheck.nat({ max: 0xffff }).map(String.fromCharCode) ``` -- `FastCheck.constant` -> `FastCheck.constant`: Import FastCheck from effect/testing. The API remains; v4 infers literal types by default. +- `FastCheck.check` -> `FastCheck.check`: Depend on fast-check and import it directly. The runner remains, but RunDetails.error was replaced by errorInstance in fast-check v4. + +- `FastCheck.clone`: TODO: needs guidance + +- `FastCheck.cloneIfNeeded`: TODO: needs guidance + +- `FastCheck.cloneMethod`: TODO: needs guidance + +- `FastCheck.commands`: TODO: needs guidance + +- `FastCheck.compareBooleanFunc`: TODO: needs guidance + +- `FastCheck.compareFunc`: TODO: needs guidance + +- `FastCheck.configureGlobal`: TODO: needs guidance + +- `FastCheck.constant` -> `FastCheck.constant`: Depend on fast-check and import it directly. The API remains; v4 infers literal types by default. + +- `FastCheck.constantFrom`: TODO: needs guidance + +- `FastCheck.context` -> `FastCheck.context`: Depend on fast-check and import it directly. The API is otherwise unchanged. + +- `FastCheck.createDepthIdentifier`: TODO: needs guidance + +- `FastCheck.date`: TODO: needs guidance + +- `FastCheck.defaultReportMessage`: TODO: needs guidance + +- `FastCheck.dictionary`: TODO: needs guidance + +- `FastCheck.domain`: TODO: needs guidance + +- `FastCheck.double`: TODO: needs guidance -- `FastCheck.context` -> `FastCheck.context`: Import FastCheck from effect/testing. The API is otherwise unchanged. +- `FastCheck.emailAddress`: TODO: needs guidance + +- `FastCheck.falsy`: TODO: needs guidance + +- `FastCheck.float`: TODO: needs guidance + +- `FastCheck.float32Array`: TODO: needs guidance + +- `FastCheck.float64Array`: TODO: needs guidance #### `FastCheck.fullUnicode` **Replacement:** `FastCheck.string` -Import FastCheck from effect/testing. Use a one-unit binary Unicode string. +Depend on fast-check and import it directly. Use a one-unit binary Unicode string. **Example** @@ -10272,7 +10738,7 @@ FastCheck.string({ unit: "binary", minLength: 1, maxLength: 1 }) **Replacement:** `FastCheck.string` -Import FastCheck from effect/testing. Use the binary string unit. +Depend on fast-check and import it directly. Use the binary string unit. **Example** @@ -10280,19 +10746,111 @@ Import FastCheck from effect/testing. Use the binary string unit. FastCheck.string({ ...constraints, unit: "binary" }) ``` -- `FastCheck.hexa` -> `FastCheck.integer`: Import FastCheck from effect/testing. Map an integer from 0 through 15 to a hexadecimal character. +- `FastCheck.func`: TODO: needs guidance + +- `FastCheck.gen`: TODO: needs guidance + +- `FastCheck.getDepthContextFor`: TODO: needs guidance + +- `FastCheck.hasAsyncToStringMethod`: TODO: needs guidance + +- `FastCheck.hasCloneMethod`: TODO: needs guidance + +- `FastCheck.hasToStringMethod`: TODO: needs guidance + +- `FastCheck.hash`: TODO: needs guidance + +- `FastCheck.hexa` -> `FastCheck.integer`: Depend on fast-check and import it directly. Map an integer from 0 through 15 to a hexadecimal character. + +- `FastCheck.hexaString` -> `FastCheck.string`: Depend on fast-check and import it directly. Pass a hexadecimal-character arbitrary as the string unit. + +- `FastCheck.infiniteStream`: TODO: needs guidance + +- `FastCheck.int16Array`: TODO: needs guidance + +- `FastCheck.int32Array`: TODO: needs guidance + +- `FastCheck.int8Array`: TODO: needs guidance + +- `FastCheck.integer`: TODO: needs guidance + +- `FastCheck.ipV4`: TODO: needs guidance + +- `FastCheck.ipV4Extended`: TODO: needs guidance + +- `FastCheck.ipV6`: TODO: needs guidance + +- `FastCheck.json`: TODO: needs guidance + +- `FastCheck.jsonValue`: TODO: needs guidance + +- `FastCheck.letrec`: TODO: needs guidance + +- `FastCheck.limitShrink`: TODO: needs guidance + +- `FastCheck.lorem`: TODO: needs guidance + +- `FastCheck.mapToConstant`: TODO: needs guidance + +- `FastCheck.maxSafeInteger`: TODO: needs guidance + +- `FastCheck.maxSafeNat`: TODO: needs guidance + +- `FastCheck.memo`: TODO: needs guidance + +- `FastCheck.mixedCase`: TODO: needs guidance + +- `FastCheck.modelRun`: TODO: needs guidance + +- `FastCheck.nat`: TODO: needs guidance + +- `FastCheck.noBias`: TODO: needs guidance + +- `FastCheck.noShrink`: TODO: needs guidance + +- `FastCheck.object`: TODO: needs guidance + +- `FastCheck.oneof`: TODO: needs guidance + +- `FastCheck.option`: TODO: needs guidance + +- `FastCheck.pre`: TODO: needs guidance -- `FastCheck.hexaString` -> `FastCheck.string`: Import FastCheck from effect/testing. Pass a hexadecimal-character arbitrary as the string unit. +- `FastCheck.property`: TODO: needs guidance -- `FastCheck.stream` -> `FastCheck.stream`: Import FastCheck from effect/testing. The API remains; update custom generator and Random implementations for fast-check v4 typings. +- `FastCheck.readConfigureGlobal`: TODO: needs guidance -- `FastCheck.string16bits` -> `FastCheck.string`: Import FastCheck from effect/testing. Pass a char16bits-compatible arbitrary as the string unit. +- `FastCheck.record`: TODO: needs guidance + +- `FastCheck.resetConfigureGlobal` -> `Undici.install`: TODO: needs guidance + +- `FastCheck.sample`: TODO: needs guidance + +- `FastCheck.scheduledModelRun`: TODO: needs guidance + +- `FastCheck.scheduler`: TODO: needs guidance + +- `FastCheck.schedulerFor`: TODO: needs guidance + +- `FastCheck.shuffledSubarray`: TODO: needs guidance + +- `FastCheck.sparseArray`: TODO: needs guidance + +- `FastCheck.statistics`: TODO: needs guidance + +- `FastCheck.stream` -> `FastCheck.stream`: Depend on fast-check and import it directly. The API remains; update custom generator and Random implementations for fast-check v4 typings. + +- `FastCheck.string`: TODO: needs guidance + +- `FastCheck.string16bits` -> `FastCheck.string`: Depend on fast-check and import it directly. Pass a char16bits-compatible arbitrary as the string unit. + +- `FastCheck.stringMatching`: TODO: needs guidance #### `FastCheck.stringOf` **Replacement:** `FastCheck.string` -Import FastCheck from effect/testing. Pass the former character arbitrary as the unit constraint. +Depend on fast-check and import it directly. Pass the former character arbitrary as the unit constraint. **Example** @@ -10300,13 +10858,29 @@ Import FastCheck from effect/testing. Pass the former character arbitrary as the FastCheck.string({ ...constraints, unit: arbitrary }) ``` -- `FastCheck.unicode` -> `FastCheck.integer`: Import FastCheck from effect/testing. Map BMP code points while excluding surrogate code points; prefer the binary string unit for full Unicode. +- `FastCheck.subarray`: TODO: needs guidance + +- `FastCheck.toStringMethod`: TODO: needs guidance + +- `FastCheck.tuple`: TODO: needs guidance + +- `FastCheck.uint16Array`: TODO: needs guidance + +- `FastCheck.uint32Array`: TODO: needs guidance + +- `FastCheck.uint8Array`: TODO: needs guidance + +- `FastCheck.uint8ClampedArray`: TODO: needs guidance + +- `FastCheck.ulid`: TODO: needs guidance + +- `FastCheck.unicode` -> `FastCheck.integer`: Depend on fast-check and import it directly. Map BMP code points while excluding surrogate code points; prefer the binary string unit for full Unicode. #### `FastCheck.unicodeJson` **Replacement:** `FastCheck.json` -Import FastCheck from effect/testing. Select binary or grapheme strings with stringUnit. +Depend on fast-check and import it directly. Select binary or grapheme strings with stringUnit. **Example** @@ -10318,7 +10892,7 @@ FastCheck.json({ stringUnit: "binary" }) **Replacement:** `FastCheck.jsonValue` -Import FastCheck from effect/testing. Select binary or grapheme strings with stringUnit. +Depend on fast-check and import it directly. Select binary or grapheme strings with stringUnit. **Example** @@ -10326,13 +10900,17 @@ Import FastCheck from effect/testing. Select binary or grapheme strings with str FastCheck.jsonValue({ stringUnit: "binary" }) ``` -- `FastCheck.unicodeString` -> `FastCheck.string`: Import FastCheck from effect/testing. Pass a BMP-code-point arbitrary as the unit constraint; prefer unit: binary for full Unicode. +- `FastCheck.unicodeString` -> `FastCheck.string`: Depend on fast-check and import it directly. Pass a BMP-code-point arbitrary as the unit constraint; prefer unit: binary for full Unicode. + +- `FastCheck.uniqueArray`: TODO: needs guidance + +- `FastCheck.uuid`: TODO: needs guidance #### `FastCheck.uuidV` **Replacement:** `FastCheck.uuid` -Import FastCheck from effect/testing. Specify the UUID version through constraints. +Depend on fast-check and import it directly. Specify the UUID version through constraints. **Example** @@ -10340,6 +10918,18 @@ Import FastCheck from effect/testing. Specify the UUID version through constrain FastCheck.uuid({ version: 4 }) ``` +- `FastCheck.webAuthority`: TODO: needs guidance + +- `FastCheck.webFragments`: TODO: needs guidance + +- `FastCheck.webPath`: TODO: needs guidance + +- `FastCheck.webQueryParameters`: TODO: needs guidance + +- `FastCheck.webSegment`: TODO: needs guidance + +- `FastCheck.webUrl`: TODO: needs guidance + ### `effect/Fiber` - `Fiber.Fiber` -> `Fiber.Fiber`: The v4 Fiber is the concrete runtime handle and is no longer itself an Effect; use Fiber.join or Fiber.await. @@ -10702,9 +11292,9 @@ FastCheck.uuid({ version: 4 }) - `Graph.Graph` -> `Graph.Graph`: The immutable type remains, but storage is opaque; replace field access with Graph nodes, edges, count, lookup, neighbor, and acyclicity APIs. -- `Graph.MutableGraph` -> `Graph.MutableGraph`: The mutable type remains but no longer extends Graph.Proto; obtain it through Graph.mutate or Graph.beginMutation and use public mutation/query functions. +- `Graph.MutableGraph` -> `Graph.MutableGraph`: The mutable type remains but no longer shares a public base interface with immutable Graph; obtain it through Graph.mutate or Graph.beginMutation and use public mutation/query functions. -- `Graph.Proto` -> `Graph.Proto`: The name remains as the opaque immutable graph protocol; it no longer exposes storage and is no longer the base of MutableGraph. +- `Graph.Proto` -> `none`: The common graph protocol was removed. Use Graph.Graph or Graph.MutableGraph as appropriate and replace storage-field access with public graph query and mutation functions. - `Graph.SearchConfig` -> `Graph.SearchConfig`: The type remains; direction is now Graph.TraversalDirection and also accepts undirected, while radius limits traversal depth. @@ -11003,7 +11593,7 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `Layer.setVersionMismatchErrorLogLevel` -> `none`: No version-mismatch log-level Reference or public replacement exists. -- `Layer.tapErrorCause` -> `Layer.tapCause`: The cause observer was renamed. +- `Layer.tapErrorCause` -> `Layer.tapCause`: The cause observer was renamed. Its callback must accept the source layer's full error cause; a callback narrowed to only part of the error union is rejected. - `Layer.toRuntime` -> `Layer.build(self), then Effect.runForkWith, Effect.runPromiseWith, or Effect.runSyncWith`: Runtime\ was removed; build a Context, or use ManagedRuntime.make for a reusable managed runner. @@ -11021,7 +11611,7 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `LayerMap.LayerMap` -> `LayerMap.LayerMap`: The type remains; runtime(key) became contextEffect(key) and returns Context. -- `LayerMap.Service` -> `LayerMap.Service`: Use layer instead of Default, layerNoDeps instead of DefaultWithoutDependencies, and contextEffect instead of runtime. +- `LayerMap.Service` -> `LayerMap.Service`: Use layer instead of Default, layerNoDeps instead of DefaultWithoutDependencies, and contextEffect instead of runtime. Preloading does not remove acquisition errors from later lookups, which can reacquire expired or invalidated entries. - `LayerMap.Service.Context` -> `LayerMap.Service.Services`: The input-services extractor was renamed. @@ -11059,6 +11649,8 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `List.filterMap` -> `Array.filterMap`: List was removed; use Array.filterMap and change the callback from Option to Result. +- `List.flatMap`: TODO: needs guidance + - `List.fromIterable` -> `Array.fromIterable`: List was removed; use Array.fromIterable. It preserves ordering but returns arrays rather than persistent linked lists. - `List.getEquivalence` -> `Array.makeEquivalence`: List was removed; compare the replacement arrays with Array.makeEquivalence. @@ -11197,7 +11789,7 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `Logger.pretty` -> `Logger.layer([Logger.consolePretty(), Logger.tracerLogger])`: Logger.layer replaces the active set; include tracerLogger to preserve v3 built-in layer behavior. -- `Logger.prettyLogger` -> `Logger.consolePretty`: Direct constructor rename; call it with the same options. +- `Logger.prettyLogger` -> `Logger.consolePretty`: Renamed to consolePretty. Remove the stderr option; provide Logger.LogToStderr with true to route TTY output to console.error. Colors, formatDate, and mode remain constructor options. - `Logger.prettyLoggerDefault` -> `Logger.consolePretty()`: The prebuilt singleton became a constructor call. @@ -11225,7 +11817,7 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `Logger.withMinimumLogLevel` -> `Effect.provideService(effect, References.MinimumLogLevel, level)`: Replace the FiberRef-local helper with reference provisioning. -- `Logger.withSpanAnnotations` -> `custom Logger.make wrapper using options.fiber.currentSpan`: No transparent generic equivalent remains. Read span identity from options.fiber.currentSpan and add it to custom output as needed. +- `Logger.withSpanAnnotations` -> `custom Logger.make wrapper using options.fiber.cache.span`: No transparent generic equivalent remains. Read span identity from options.fiber.cache.span and add it to custom output as needed. - `Logger.zip` -> `Logger.make(options => [left.log(options), right.log(options)])`: No named combinator remains; invoke both loggers and return their output tuple. @@ -12213,6 +12805,8 @@ SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues - `ParseResult.ArrayFormatterIssue` -> `StandardSchemaV1.FailureResult["issues"][number]`: Use the Standard Schema issue shape returned by makeFormatterStandardSchemaV1. +- `ParseResult.Composite` -> `SchemaIssue.Composite`: Composite parse failures moved to SchemaIssue. The v4 constructor takes the failing AST and an array of nested issues; input is retained only when reportInput is enabled. + - `ParseResult.DeclarationDecodeUnknown` -> `SchemaGetter.Getter`: Custom declaration decoding now uses SchemaGetter values and Schema.declare annotations. - `ParseResult.DecodeUnknown` -> `Schema.decodeUnknownEffect`: Use the function type returned by Schema.decodeUnknownEffect. @@ -12221,16 +12815,22 @@ SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues - `ParseResult.Missing` -> `SchemaIssue.MissingKey`: Missing-key failures use the v4 SchemaIssue class. +- `ParseResult.ParseError`: TODO: needs guidance + - `ParseResult.ParseErrorTypeId` -> `none`: The public symbol was removed; use Schema.isSchemaError for runtime narrowing. - `ParseResult.ParseIssue` -> `SchemaIssue.Issue`: The structured parse issue union moved to SchemaIssue. - `ParseResult.ParseResultFormatter` -> `SchemaIssue.Formatter`: Issue formatter types moved to SchemaIssue. +- `ParseResult.Pointer` -> `SchemaIssue.Pointer`: Path-qualified failures moved to SchemaIssue. Construct them with the property path and nested issue; rejected input belongs to the nested issue when reportInput is enabled. + - `ParseResult.Refinement` -> `SchemaIssue.Filter`: Refinement failures are represented as filter issues in v4. - `ParseResult.SingleOrNonEmpty` -> `ReadonlyArray`: This ParseResult helper type was removed; use an explicit value-or-non-empty-array type when still needed. +- `ParseResult.Transformation` -> `SchemaIssue.Encoding`: Transformation-stage failures are Encoding issues in v4. They retain the failing AST and nested issue; the old Encoded, Transformation, and Type kind discriminator was removed. + #### `ParseResult.TreeFormatter` **Replacement:** `SchemaIssue.defaultFormatter` @@ -12247,6 +12847,8 @@ SchemaIssue.defaultFormatter(issue) - `ParseResult.Unexpected` -> `SchemaIssue.UnexpectedKey`: Unexpected object keys use the v4 SchemaIssue class. +- `ParseResult.decode`: TODO: needs guidance + - `ParseResult.decodeEither` -> `Schema.decodeExit`: Either parsing was replaced by Exit parsing. - `ParseResult.decodePromise` -> `Schema.decodePromise`: Parsing helpers moved onto Schema and now fail with SchemaError. @@ -12261,6 +12863,8 @@ SchemaIssue.defaultFormatter(issue) - `ParseResult.eitherOrUndefined` -> `none`: This ParseResult internal optimization was removed; use Effect, Exit, Option, or Result combinators directly. +- `ParseResult.encode`: TODO: needs guidance + - `ParseResult.encodeEither` -> `Schema.encodeExit`: Either encoding was replaced by Exit encoding. - `ParseResult.encodeSync` -> `Schema.encodeSync`: Encoding helpers moved onto Schema and now throw SchemaError. @@ -13923,9 +14527,9 @@ Schema.toFormatter(schema) - `Schema.TaggedStruct` -> `Schema.TaggedStruct`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. -- `Schema.TemplateLiteral` -> `Schema.TemplateLiteral(parts)`: Pass template literal parts as one array. +- `Schema.TemplateLiteral` -> `Schema.TemplateLiteral(parts)`: Pass template literal parts as one array. Parts must not contain encodings, including inside unions and nested templates. Transformations whose decoded and encoded types are equal are also rejected. Use Schema.TemplateLiteralParser(parts) for transformed parts. -- `Schema.TemplateLiteralParser` -> `Schema.TemplateLiteralParser(schema.parts)`: Create the template schema first and pass its `parts` property. +- `Schema.TemplateLiteralParser` -> `Schema.TemplateLiteralParser(parts)`: Pass template literal parts directly as one array. Transformed parts are supported, and their decoding and encoding services are required in the corresponding direction. - `Schema.TimeZone` -> `Schema.TimeZoneFromString`: Use the string codec; v4 `TimeZone` is the self schema. @@ -14329,11 +14933,11 @@ Schema.toFormatter(schema) - `SchemaAST.AST` -> `SchemaAST.AST`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. -- `SchemaAST.Annotated` -> `SchemaAST.Base`: All v4 AST nodes extend Base, which owns annotations, checks, encoding, and context. +- `SchemaAST.Annotated` -> `SchemaAST.AST`: The public base type was removed. Use the AST union; every variant still exposes annotations, checks, encoding, and context. - `SchemaAST.AnyKeyword` -> `SchemaAST.Any`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. -- `SchemaAST.ArbitraryAnnotationId` -> `Schema.Annotations.ToArbitrary`: Symbol annotation IDs were removed; use the toArbitrary annotation key and its Schema.Annotations types. +- `SchemaAST.ArbitraryAnnotationId` -> `Schema.Annotations.ToArbitrary`: Symbol annotation IDs were removed. Declarations use the toCodecArbitrary annotation; filters use arbitraryConstraint. - `SchemaAST.BatchingAnnotation` -> `none`: Per-schema batching annotations were removed; control asynchronous parsing with ParseOptions.concurrency. @@ -14455,7 +15059,7 @@ Schema.toFormatter(schema) - `SchemaAST.SymbolKeyword` -> `SchemaAST.Symbol`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. -- `SchemaAST.TemplateLiteral` -> `SchemaAST.TemplateLiteral`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. +- `SchemaAST.TemplateLiteral` -> `SchemaAST.TemplateLiteral`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. Parts must not contain encodings, including inside unions or nested templates; use Schema.TemplateLiteralParser for transformed parts. - `SchemaAST.TemplateLiteralSpan` -> `SchemaAST.TemplateLiteral`: Template literal parts are represented directly as AST values in v4. @@ -14607,6 +15211,8 @@ Schema.toFormatter(schema) - `SchemaAST.partial` -> `Schema.mapFields + Struct.map(Schema.optional)`: Partial object transforms moved to schema field transforms. +- `SchemaAST.pick` -> `none`: The low-level AST picker was removed. Keep field selection at the Schema.Struct level with mapFields and Struct.pick, or discriminate and rebuild custom AST nodes explicitly. + - `SchemaAST.required` -> `Schema.mapFields + Struct.map(Schema.requiredKey)`: Required object transforms moved to schema field transforms. - `SchemaAST.stringKeyword` -> `SchemaAST.string`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. @@ -14885,6 +15491,8 @@ Schema.toFormatter(schema) - `SortedSet.filter` -> `HashSet.filter`: Direct persistent filtering on the replacement set; traversal is unordered until explicitly sorted. +- `SortedSet.flatMap`: TODO: needs guidance + - `SortedSet.fromIterable` -> `HashSet.fromIterable`: Use HashSet.fromIterable and retain the element Order separately. - `SortedSet.getEquivalence` -> `Equal.asEquivalence`: HashSet implements Effect equality by set content; use Equal.asEquivalence\\>(). @@ -14907,6 +15515,8 @@ Schema.toFormatter(schema) - `SortedSet.some` -> `HashSet.some`: Run the predicate against the replacement HashSet; sort first only if traversal order has observable effects. +- `SortedSet.toggle` -> `HashSet.has + HashSet.add / HashSet.remove`: HashSet has no toggle; branch on membership and add or remove the element. + - `SortedSet.union` -> `HashSet.union + HashSet.fromIterable`: Convert the old general iterable argument to HashSet before taking the union. - `SortedSet.values` -> `Array.sort`: Sort the replacement HashSet with the retained Order and iterate the resulting array. @@ -15372,9 +15982,9 @@ switch (strategy) { ### `effect/SynchronizedRef` -- `SynchronizedRef.SynchronizedRef` -> `SynchronizedRef.SynchronizedRef`: The model remains, now extends the v4 Ref model, and is read or updated through explicit SynchronizedRef operations. +- `SynchronizedRef.SynchronizedRef` -> `SynchronizedRef.SynchronizedRef`: The model remains but no longer extends Ref; read and update it through explicit SynchronizedRef operations. The curried v4 modifySomeEffect takes only the callback, which returns an Effect of [result, Option\]; remove the v3 fallback and outer Option. -- `SynchronizedRef.SynchronizedRef.Variance` -> `Ref.Ref.Variance`: SynchronizedRef now inherits Ref variance instead of declaring a separate public variance marker. +- `SynchronizedRef.SynchronizedRef.Variance` -> `none`: The public nested variance marker was removed. SynchronizedRef uses an internal brand in v4, so do not refer to a variance interface directly. - `SynchronizedRef.SynchronizedRefTypeId` -> `none`: The SynchronizedRef type id is internal in v4; do not inspect or construct the brand directly. @@ -15920,7 +16530,7 @@ Exit.isExit(take) ### `effect/TestConfig` -- `TestConfig.TestConfig` -> `none`: There is no v4 TestConfig service. Move runner settings to Vitest and FastCheck options, or define an application-specific Context.Reference if runtime access is needed. +- `TestConfig.TestConfig` -> `none`: There is no v4 TestConfig service. Move runner settings to Vitest and native Arbitrary check options, or define an application-specific Context.Reference if runtime access is needed. - `TestConfig.make` -> `{ repeats, retries, samples, shrinks }`: The v3 constructor only returned its parameter object. The TestConfig service was removed; keep a plain object only for application-owned configuration. @@ -15968,9 +16578,9 @@ Exit.isExit(take) - `TestServices.retries` -> `Vitest TestOptions.retry`: Configure retry in Vitest test options. To retry an Effect inside a test, use Effect.retry. -- `TestServices.samples` -> `{ fastCheck: { numRuns } }`: Pass the run count through @effect/vitest property-test options, for example it.effect.prop(..., { fastCheck: { numRuns: samples } }). +- `TestServices.samples` -> `{ arbitrary: { runs } }`: Pass the run count through @effect/vitest property-test options, for example it.effect.prop(..., { arbitrary: { runs: samples } }). -- `TestServices.shrinks` -> `none`: The legacy maximum-shrinks service setting was removed; @effect/vitest forwards FastCheck.Parameters, which has no equivalent service value. +- `TestServices.shrinks` -> `{ arbitrary: { maxShrinks } }`: Pass maxShrinks through @effect/vitest property-test options, for example it.effect.prop(..., { arbitrary: { maxShrinks } }). - `TestServices.size` -> `CurrentSize`: Define a custom Context.Reference\ and yield it to read the current size. @@ -15982,11 +16592,11 @@ Exit.isExit(take) - `TestServices.supervisedFibers` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. -- `TestServices.testConfig` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference. +- `TestServices.testConfig` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test arbitrary options; model application state as a custom Context.Reference. -- `TestServices.testConfigLayer` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference. +- `TestServices.testConfigLayer` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test arbitrary options; model application state as a custom Context.Reference. -- `TestServices.testConfigWith` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference. +- `TestServices.testConfigWith` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test arbitrary options; model application state as a custom Context.Reference. - `TestServices.withAnnotations` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. @@ -16000,9 +16610,9 @@ Exit.isExit(take) - `TestServices.withSizedScoped` -> `Effect.updateServiceScoped(CurrentSize, () => size)`: For a scope-bounded override use updateServiceScoped; otherwise prefer wrapping the workflow with Effect.provideService. -- `TestServices.withTestConfig` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference. +- `TestServices.withTestConfig` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test arbitrary options; model application state as a custom Context.Reference. -- `TestServices.withTestConfigScoped` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference. +- `TestServices.withTestConfigScoped` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test arbitrary options; model application state as a custom Context.Reference. ### `effect/TestSized` diff --git a/repos/effect/package.json b/repos/effect/package.json index 8dbc701c7d..e51910a174 100644 --- a/repos/effect/package.json +++ b/repos/effect/package.json @@ -37,15 +37,15 @@ "@babel/core": "^8.0.1", "@babel/plugin-transform-export-namespace-from": "^8.0.1", "@babel/plugin-transform-modules-commonjs": "^8.0.1", - "@changesets/changelog-github": "1.0.0", - "@changesets/cli": "3.0.1", + "@changesets/changelog-github": "1.0.1", + "@changesets/cli": "3.0.2", "@effect/ai-docgen": "workspace:^", "@effect/bundle": "workspace:^", "@effect/docgen": "workspace:^", "@effect/doctest": "workspace:^", "@effect/jsdocs": "workspace:^", "@effect/oxc": "workspace:^", - "@effect/tsgo": "^0.36.5", + "@effect/tsgo": "^0.41.0", "@effect/utils": "workspace:^", "@effect/vitest": "workspace:^", "@faker-js/faker": "^10.6.0", @@ -53,32 +53,31 @@ "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^1.0.0", "@types/jscodeshift": "^17.3.0", - "@types/node": "^26.2.0", - "@vitest/browser": "^4.1.11", - "@vitest/coverage-v8": "^4.1.11", - "@vitest/expect": "^4.1.11", - "@vitest/web-worker": "^4.1.11", - "ast-types": "^0.14.2", + "@types/node": "^26.4.1", + "@vitest/coverage-v8": "^5.0.0", + "@vitest/web-worker": "^5.0.0", + "ast-types": "^0.16.3", "babel-plugin-annotate-pure-calls": "^0.5.0", - "dprint": "^0.56.1", + "dprint": "^0.57.3", + "fast-check": "^4.9.0", "glob": "^13.0.6", - "happy-dom": "^20.11.6", + "happy-dom": "^20.14.0", "jscodeshift": "^17.4.0", "lalph": "^0.3.139", "madge": "^8.0.0", - "oxlint": "^1.79.0", + "oxlint": "^1.81.0", "pkg-pr-new": "0.0.88", - "playwright": "^1.62.1", - "rollup": "^4.62.5", + "playwright": "^1.63.0", + "rollup": "^4.63.1", "rollup-plugin-bundle-stats": "^4.22.3", "rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-visualizer": "^7.1.1", - "terser": "^5.50.0", - "tstyche": "^7.2.3", + "terser": "^5.51.2", + "tstyche": "^7.2.4", "typescript": "^7.0.2", "vite": "^8.2.2", - "vitest": "^4.1.11", + "vitest": "^5.0.0", "vitest-websocket-mock": "^0.7.0", - "zod": "^4.4.3" + "zod": "^4.5.4" } } diff --git a/repos/effect/packages/ai/anthropic/codegen.yml b/repos/effect/packages/ai/anthropic/codegen.yml index dd23c4bcb1..e112fc77af 100644 --- a/repos/effect/packages/ai/anthropic/codegen.yml +++ b/repos/effect/packages/ai/anthropic/codegen.yml @@ -13,6 +13,7 @@ header: | */ excludeAnnotations: - examples +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 @@ -59,4 +60,3 @@ replacements: # Make context_management optional in BetaMessageDeltaEvent (API may omit this field) - from: '"context_management": Schema.Union([BetaResponseContextManagement, Schema.Null]).annotate({ "description": "Information about context management strategies applied during the request", "default": null }),' to: '"context_management": Schema.optionalKey(Schema.Union([BetaResponseContextManagement, Schema.Null]).annotate({ "description": "Information about context management strategies applied during the request", "default": null })),' - diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts b/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts index 7ba02b588d..a0eae898c9 100644 --- a/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts +++ b/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts @@ -20,7 +20,6 @@ import * as Predicate from "effect/Predicate" import * as Redactable from "effect/Redactable" import * as Schema from "effect/Schema" import * as SchemaAST from "effect/SchemaAST" -import * as SchemaIssue from "effect/SchemaIssue" import * as Stream from "effect/Stream" import type { Span } from "effect/Tracer" import type { Mutable, Simplify } from "effect/Types" @@ -40,8 +39,6 @@ import type { AnthropicTool } from "./AnthropicTool.ts" import type * as Generated from "./Generated.ts" import * as InternalUtilities from "./internal/utilities.ts" -const formatIssue = SchemaIssue.makeFormatterDefault() - /** * Known Anthropic Claude model identifiers exposed by the generated Anthropic schema. * @@ -713,8 +710,13 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig if (betas.size > 0) { params["anthropic-beta"] = Array.from(betas).join(",") } - const { disableParallelToolCalls: _, output_config, structuredOutputs: _structuredOutputs, ...requestConfig } = - config + const { + disableParallelToolCalls: _, + output_config, + strictJsonSchema: _strictJsonSchema, + structuredOutputs: _structuredOutputs, + ...requestConfig + } = config const payload: Mutable = { ...requestConfig, max_tokens: requestConfig.max_tokens ?? capabilities.maxOutputTokens, @@ -892,7 +894,13 @@ const prepareMessages = Effect.fnUntraced( const source = isUrlData(part.data) ? { type: "url", url: getUrlString(part.data) } as const - : { type: "base64", media_type: mediaType, data: Encoding.encodeBase64(part.data) } as const + : { + type: "base64", + media_type: mediaType, + data: typeof part.data === "string" + ? part.data.replace(/^data:[^;]+;base64,/, "") + : Encoding.encodeBase64(part.data) + } as const content.push({ type: "image", source, cache_control: cacheControl }) } else if (part.mediaType === "application/pdf" || part.mediaType === "text/plain") { @@ -965,7 +973,7 @@ const prepareMessages = Effect.fnUntraced( content.push({ type: "tool_result", tool_use_id: part.id, - content: JSON.stringify(part.result), + content: typeof part.result === "string" ? part.result : JSON.stringify(part.result), is_error: part.isFailure, cache_control: cacheControl }) @@ -1290,10 +1298,6 @@ const prepareTools = Effect.fnUntraced( readonly tools: ReadonlyArray | undefined readonly toolChoice: typeof Generated.BetaToolChoice.Encoded | undefined }, AiError.AiError> { - if (options.tools.length === 0 || options.toolChoice === "none") { - return { tools: undefined, toolChoice: undefined } - } - // Return a JSON response tool when using non-native structured outputs if (options.responseFormat.type === "json" && !capabilities.supportsStructuredOutput) { const input_schema = yield* tryJsonSchema(options.responseFormat.schema, "prepareTools") @@ -1313,6 +1317,10 @@ const prepareTools = Effect.fnUntraced( } } + if (options.tools.length === 0 || options.toolChoice === "none") { + return { tools: undefined, toolChoice: undefined } + } + const userTools: Array = [] const providerTools: Array = [] @@ -1567,6 +1575,9 @@ const makeResponse = Effect.fnUntraced( const mcpToolCalls: Map = new Map() const serverToolCalls: Map = new Map() const citableDocuments = extractCitableDocuments(options.prompt) + const responseFormat = options.responseFormat + const hasStructuredOutputTool = responseFormat.type === "json" && + rawResponse.content.some((part) => part.type === "tool_use" && part.name === responseFormat.objectName) parts.push({ type: "response-metadata", @@ -1579,10 +1590,12 @@ const makeResponse = Effect.fnUntraced( for (const part of rawResponse.content) { switch (part.type) { case "text": { - // Text parts are added for both text and json response formats. - // For native structured output (json_schema), the JSON comes directly - // in a text content block. For tool-based structured output, text may - // also be present alongside the tool_use. + // The response tool supplies the JSON payload. Accompanying prose + // must not be concatenated with it during structured output decoding. + if (hasStructuredOutputTool) { + break + } + parts.push({ type: "text", text: part.text @@ -1629,7 +1642,7 @@ const makeResponse = Effect.fnUntraced( case "tool_use": { // When the `"json"` response format is requested, the JSON we need // is returned by a tool call injected into the request - if (options.responseFormat.type === "json") { + if (responseFormat.type === "json" && part.name === responseFormat.objectName) { parts.push({ type: "text", text: JSON.stringify(part.input) @@ -3106,19 +3119,13 @@ const transformToolCallParams = Effect.fnUntraced(function* - ).pipe(Effect.mapError((error) => - AiError.make({ - module: "AnthropicLanguageModel", - method: "makeResponse", - reason: new AiError.ToolParameterValidationError({ - toolName, - toolParams, - description: formatIssue(error.issue) - }) - }) - )) + Schema.decodeEffect(codec)(toolParams) as Effect.Effect + ).pipe( + Effect.flatMap((decoded) => + Schema.encodeUnknownEffect(tool.parametersSchema)(decoded) as Effect.Effect + ), + Effect.orElseSucceed(() => toolParams) + ) }) diff --git a/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts b/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts index bd8b190232..56bab6b2dc 100644 --- a/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts +++ b/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts @@ -2,6 +2,7 @@ import { AnthropicClient, AnthropicLanguageModel, AnthropicTool } from "@effect/ import { assert, describe, it } from "@effect/vitest" import { Effect, Layer, Redacted, Schema, Stream } from "effect" import { + type AiError, AnthropicStructuredOutput, LanguageModel, Prompt, @@ -119,6 +120,113 @@ describe("AnthropicLanguageModel", () => { assert.deepStrictEqual(toolCall.params, toolParams) })) + it.effect("routes invalid tool call params through failureMode: return without failing the stream", () => + Effect.gen(function*() { + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + type: "message_start", + message: { + id: "msg_test_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 10, + output_tokens: 0, + service_tier: null + } + } + }, + { + type: "content_block_start", + index: 0, + content_block: { + type: "tool_use", + id: "toolu_test_1", + name: "GlobTool", + input: {} + } + }, + { + type: "content_block_delta", + index: 0, + delta: { + type: "input_json_delta", + partial_json: JSON.stringify({ pattern: 123 }) + } + }, + { + type: "content_block_stop", + index: 0 + }, + { + type: "message_delta", + delta: { + stop_reason: "tool_use", + stop_sequence: null + }, + usage: { + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + input_tokens: null, + output_tokens: 5 + } + }, + { + type: "message_stop" + } + ])) + ) + )) + ) + + const GlobTool = Tool.make("GlobTool", { + description: "Search for files", + failureMode: "return", + parameters: Schema.Struct({ pattern: Schema.String }), + success: Schema.String, + failure: Schema.String + }) + + const toolkit = Toolkit.make(GlobTool) + const toolkitLayer = toolkit.toLayer({ + GlobTool: () => Effect.succeed("found.ts") + }) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "find ts files", + toolkit + }).pipe( + Stream.runCollect, + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + const toolResult = parts.find((part) => part.type === "tool-result") + assert.isDefined(toolResult) + if (toolResult?.type !== "tool-result") { + return + } + + assert.strictEqual(toolResult.isFailure, true) + const failure = toolResult.result as AiError.AiError + assert.strictEqual(failure._tag, "AiError") + assert.strictEqual(failure.reason._tag, "ToolParameterValidationError") + })) + const codeExecutionCases = [ { providerName: "bash_code_execution", @@ -329,6 +437,175 @@ describe("AnthropicLanguageModel", () => { }) describe("generateText", () => { + it.effect("omits strictJsonSchema from the request while preserving tool strictness", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined + const model = "claude-sonnet-4-5" + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + id: "msg_test_1", + type: "message", + role: "assistant", + model, + content: [{ type: "text", text: "Hello" }], + stop_reason: "end_turn", + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 1, + output_tokens: 1, + service_tier: null + } + })) + }) + )) + ) + + yield* LanguageModel.generateText({ + prompt: "Hello", + toolkit: Toolkit.make(Tool.make("Search", { + parameters: Schema.Struct({ query: Schema.String }), + success: Schema.String + })), + disableToolCallResolution: true + }).pipe( + Effect.provide(AnthropicLanguageModel.model(model, { strictJsonSchema: false })), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + const body = yield* getRequestBody(capturedRequest) + assert.strictEqual(body.model, model) + assert.strictEqual(body.tools[0].name, "Search") + assert.strictEqual(body.tools[0].strict, false) + assert.notProperty(body, "strictJsonSchema") + })) + + it.effect("preserves string tool results", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + id: "msg_test_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [{ type: "text", text: "Done" }], + stop_reason: "end_turn", + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 10, + output_tokens: 5, + service_tier: null + } + })) + }) + )) + ) + + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Use the tool" }, + { + role: "assistant", + content: [Prompt.toolCallPart({ + id: "call_text", + name: "text_tool", + params: {}, + providerExecuted: false + })] + }, + { + role: "tool", + content: [Prompt.toolResultPart({ + id: "call_text", + name: "text_tool", + result: "PLAIN_TEXT_SENTINEL\n", + isFailure: false, + providerExecuted: false + })] + } + ]), + disableToolCallResolution: true + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const body = yield* getRequestBody(capturedRequest) + const toolResult = body.messages + .flatMap((message: any) => Array.isArray(message.content) ? message.content : []) + .find((block: any) => block.type === "tool_result") + + assert.isDefined(toolResult) + assert.strictEqual(toolResult.content, "PLAIN_TEXT_SENTINEL\n") + })) + + it.effect("preserves base64 image string payloads", () => + Effect.gen(function*() { + const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGP4DwQACfsD/fteaysAAAAASUVORK5CYII=" + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + id: "msg_test_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [{ type: "text", text: "Done" }], + stop_reason: "end_turn", + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 1, + output_tokens: 1, + service_tier: null + } + })) + }) + )) + ) + + yield* LanguageModel.generateText({ + prompt: Prompt.make([Prompt.userMessage({ + content: [Prompt.filePart({ mediaType: "image/png", data: base64 })] + })]) + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + const body = yield* getRequestBody(capturedRequest) + assert.strictEqual(body.messages[0].content[0].source.data, base64) + })) + it.effect("encodes dynamic tools", () => Effect.gen(function*() { let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined @@ -644,6 +921,94 @@ describe("AnthropicLanguageModel", () => { assert.strictEqual(body.output_config?.format?.type, "json_schema") assert.notProperty(body, "structuredOutputs") })) + + const summaryResponse = (request: HttpClientRequest.HttpClientRequest, content: ReadonlyArray) => + jsonResponse(request, { + id: "msg_summary", + type: "message", + role: "assistant", + model: "claude-sonnet-4-6", + content, + stop_reason: "tool_use", + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 1, + output_tokens: 1, + service_tier: null + } + }) + + it.effect("forces the fallback response tool and decodes its input alongside prose", () => + Effect.gen(function*() { + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.gen(function*() { + const body = yield* getRequestBody(request) + assert.deepStrictEqual(body.tools?.map((tool: { name: string }) => tool.name), ["summary"]) + assert.deepStrictEqual(body.tool_choice, { + type: "tool", + name: "summary", + disable_parallel_tool_use: true + }) + return summaryResponse(request, [ + { type: "text", text: "Here is the summary." }, + { type: "tool_use", id: "toolu_summary", name: "summary", input: { title: "Rain" } } + ]) + }) + ) + )) + ) + + const response = yield* LanguageModel.generateObject({ + prompt: "Give a title for a story about rain.", + objectName: "summary", + schema: Schema.Struct({ title: Schema.String }) + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-6", { structuredOutputs: false })), + Effect.provide(layer) + ) + + assert.deepStrictEqual(response.value, { title: "Rain" }) + })) + + it.effect("decodes native JSON alongside an ordinary tool call", () => + Effect.gen(function*() { + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(summaryResponse(request, [ + { type: "text", text: JSON.stringify({ title: "Rain" }) }, + { type: "tool_use", id: "toolu_weather", name: "Weather", input: { city: "SF" } } + ])) + ) + )) + ) + const toolkit = Toolkit.make(Tool.make("Weather", { + parameters: Schema.Struct({ city: Schema.String }), + success: Schema.String + })) + + const response = yield* LanguageModel.generateObject({ + prompt: "Give a title for a story about rain.", + objectName: "summary", + schema: Schema.Struct({ title: Schema.String }), + toolkit + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-6")), + Effect.provide(toolkit.toLayer({ Weather: () => Effect.succeed("Rain") })), + Effect.provide(layer) + ) + + assert.deepStrictEqual(response.value, { title: "Rain" }) + assert.strictEqual(response.toolCalls[0]?.name, "Weather") + })) }) // The packaged `Memory_20250818` tool ships `customName: "AnthropicMemory"` / diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts b/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts index 71e1e586aa..099acc58ff 100644 --- a/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts +++ b/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts @@ -20,7 +20,6 @@ import * as Rec from "effect/Record" import * as Redactable from "effect/Redactable" import * as Schema from "effect/Schema" import * as AST from "effect/SchemaAST" -import * as SchemaIssue from "effect/SchemaIssue" import * as Stream from "effect/Stream" import type { Span } from "effect/Tracer" import type { DeepMutable, Simplify } from "effect/Types" @@ -55,8 +54,6 @@ import { } from "./OpenAiClient.ts" import { addGenAIAnnotations } from "./OpenAiTelemetry.ts" -const formatIssue = SchemaIssue.makeFormatterDefault() - /** * Image detail level for vision requests. */ @@ -790,15 +787,14 @@ const prepareMessages = Effect.fnUntraced( if (typeof part.data === "string" && isFileId(part.data, config)) { content.push({ type: "input_image", file_id: part.data, detail }) - } - - if (part.data instanceof URL) { - content.push({ type: "input_image", image_url: part.data.toString(), detail }) - } - - if (part.data instanceof Uint8Array) { - const base64 = Encoding.encodeBase64(part.data) - const imageUrl = `data:${mediaType};base64,${base64}` + } else { + const imageUrl = part.data instanceof URL + ? part.data.toString() + : part.data instanceof Uint8Array + ? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}` + : /^(data:|https?:\/\/)/i.test(part.data) + ? part.data + : `data:${mediaType};base64,${part.data}` content.push({ type: "input_image", image_url: imageUrl, detail }) } } else if (part.mediaType === "application/pdf") { @@ -1099,7 +1095,6 @@ const makeResponse = Effect.fnUntraced( method: "makeResponse", reason: new AiError.ToolParameterValidationError({ toolName, - toolParams: {}, description: `Failed to securely JSON parse tool parameters: ${cause}` }) }) @@ -1192,7 +1187,6 @@ const makeStreamResponse = Effect.fnUntraced( method: "makeStreamResponse", reason: new AiError.ToolParameterValidationError({ toolName: toolCall.name, - toolParams: {}, description: `Failed to securely JSON parse tool parameters: ${cause}` }) }) @@ -1452,21 +1446,16 @@ const transformToolCallParams = Effect.fnUntraced(function* - ).pipe(Effect.mapError((error) => - AiError.make({ - module: "OpenAiLanguageModel", - method: "makeResponse", - reason: new AiError.ToolParameterValidationError({ - toolName, - toolParams, - description: formatIssue(error.issue) - }) - }) - )) + Schema.decodeEffect(codec)(toolParams) as Effect.Effect + ).pipe( + Effect.flatMap((decoded) => + Schema.encodeUnknownEffect(tool.parametersSchema)(decoded) as Effect.Effect + ), + Effect.orElseSucceed(() => toolParams) + ) }) const prepareTools = Effect.fnUntraced(function*>({ diff --git a/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts b/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts index df6edf9234..dc90d1f4d1 100644 --- a/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts +++ b/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts @@ -1,7 +1,7 @@ import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai-compat" import { assert, describe, it } from "@effect/vitest" import { Effect, Layer, Redacted, Ref, Schema, Stream } from "effect" -import { LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai" +import { type AiError, LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai" import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("OpenAiLanguageModel", () => { @@ -48,6 +48,67 @@ describe("OpenAiLanguageModel", () => { assert.strictEqual(requestBody.messages[0]?.content, "hello") })) + it.effect("routes invalid tool call params through failureMode: return without failing the effect", () => + Effect.gen(function*() { + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "tool_calls", + message: { + role: "assistant", + content: null, + tool_calls: [{ + id: "call_1", + type: "function", + function: { + name: "ReturnModeTool", + arguments: JSON.stringify({ input: 123 }) + } + }] + } + }] + }) + )) + ) + )) + ) + + const ReturnModeTool = Tool.make("ReturnModeTool", { + description: "A test tool", + failureMode: "return", + parameters: Schema.Struct({ input: Schema.String }), + success: Schema.Struct({ output: Schema.String }), + failure: Schema.Struct({ error: Schema.String }) + }) + + const toolkit = Toolkit.make(ReturnModeTool) + const toolkitLayer = toolkit.toLayer({ + ReturnModeTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` }) + }) + + const result = yield* LanguageModel.generateText({ + prompt: "use the tool", + toolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + assert.strictEqual(result.toolResults.length, 1) + const toolResult = result.toolResults[0]! + assert.strictEqual(toolResult.isFailure, true) + const failure = toolResult.result as AiError.AiError + assert.strictEqual(failure._tag, "AiError") + assert.strictEqual(failure.reason._tag, "ToolParameterValidationError") + })) + it.effect("forwards reasoning config to chat completions request", () => Effect.gen(function*() { let capturedRequest: HttpClientRequest.HttpClientRequest | undefined @@ -142,8 +203,9 @@ describe("OpenAiLanguageModel", () => { }) })) - it.effect("preserves multimodal user content order in chat payload", () => + it.effect("preserves URL and base64 images in multimodal content order", () => Effect.gen(function*() { + const base64 = "iVBORw0KGgo=" let capturedRequest: HttpClientRequest.HttpClientRequest | undefined const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( @@ -177,6 +239,8 @@ describe("OpenAiLanguageModel", () => { mediaType: "image/png", data: new URL("https://example.com/image.png") }), + Prompt.filePart({ mediaType: "image/png", data: "https://example.com/string-image.png" }), + Prompt.filePart({ mediaType: "image/png", data: base64 }), Prompt.textPart({ text: "second text" }) ] }]) @@ -205,6 +269,20 @@ describe("OpenAiLanguageModel", () => { detail: "auto" } }, + { + type: "image_url", + image_url: { + url: "https://example.com/string-image.png", + detail: "auto" + } + }, + { + type: "image_url", + image_url: { + url: `data:image/png;base64,${base64}`, + detail: "auto" + } + }, { type: "text", text: "second text" diff --git a/repos/effect/packages/ai/openai/src/OpenAiClient.ts b/repos/effect/packages/ai/openai/src/OpenAiClient.ts index 43753b63b4..21b4f38e6b 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiClient.ts +++ b/repos/effect/packages/ai/openai/src/OpenAiClient.ts @@ -510,9 +510,9 @@ const makeSocket = Effect.gen(function*() { Effect.provideService(Socket.WebSocketConstructor, (url) => makeWebSocket(url, { headers: request.headers - } as any)) + })) ) - const write = yield* socket.writer + const writer = yield* socket.writer yield* Scope.addFinalizerExit(scope, () => { tracker.clearUnsafe() @@ -521,7 +521,7 @@ const makeSocket = Effect.gen(function*() { const incoming = yield* Queue.unbounded() const send = (message: typeof OpenAiSchema.CreateResponse.Encoded) => - write(JSON.stringify({ + writer.write(JSON.stringify({ type: "response.create", ...message })).pipe( @@ -544,7 +544,7 @@ const makeSocket = Effect.gen(function*() { ) ) - yield* socket.runRaw((msg) => { + const handleMessage = (msg: Uint8Array | string): Effect.Effect | undefined => { const text = typeof msg === "string" ? msg : decoder.decode(msg) try { const event = decodeEvent(text) @@ -580,7 +580,22 @@ const makeSocket = Effect.gen(function*() { } Queue.offerUnsafe(incoming, event) } catch {} + return undefined + } + + yield* Effect.gen(function*() { + const { pull } = yield* socket.reader + while (true) { + const messages = yield* pull + for (let i = 0; i < messages.length; i++) { + const result = handleMessage(messages[i]) + if (result !== undefined) { + yield* result + } + } + } }).pipe( + Effect.scoped, Effect.catchTag("SocketError", (error) => AiError.make({ module: "OpenAiClient", diff --git a/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts b/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts index 4addf9f5bf..5635f38361 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts +++ b/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts @@ -19,7 +19,6 @@ import * as Predicate from "effect/Predicate" import * as Redactable from "effect/Redactable" import * as Schema from "effect/Schema" import * as AST from "effect/SchemaAST" -import * as SchemaIssue from "effect/SchemaIssue" import * as Stream from "effect/Stream" import type { Span } from "effect/Tracer" import type { DeepMutable, Mutable, Simplify } from "effect/Types" @@ -40,8 +39,6 @@ import type * as OpenAiSchema from "./OpenAiSchema.ts" import { addGenAIAnnotations } from "./OpenAiTelemetry.ts" import type * as OpenAiTool from "./OpenAiTool.ts" -const formatIssue = SchemaIssue.makeFormatterDefault() - const ResponseModelIds = Generated.ModelIdsResponses.members[1] const SharedModelIds = Generated.ModelIdsShared.members[1] @@ -881,15 +878,14 @@ const prepareMessages = Effect.fnUntraced( if (typeof part.data === "string" && isFileId(part.data, config)) { content.push({ type: "input_image", file_id: part.data, detail }) - } - - if (part.data instanceof URL) { - content.push({ type: "input_image", image_url: part.data.toString(), detail }) - } - - if (part.data instanceof Uint8Array) { - const base64 = Encoding.encodeBase64(part.data) - const imageUrl = `data:${mediaType};base64,${base64}` + } else { + const imageUrl = part.data instanceof URL + ? part.data.toString() + : part.data instanceof Uint8Array + ? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}` + : /^(data:|https?:\/\/)/i.test(part.data) + ? part.data + : `data:${mediaType};base64,${part.data}` content.push({ type: "input_image", image_url: imageUrl, detail }) } } else if (part.mediaType === "application/pdf") { @@ -1045,7 +1041,6 @@ const prepareMessages = Effect.fnUntraced( method: "prepareMessages", reason: new AiError.ToolParameterValidationError({ toolName: "local_shell", - toolParams: part.params as Schema.Json, description: error.message }) }) @@ -1071,7 +1066,6 @@ const prepareMessages = Effect.fnUntraced( method: "prepareMessages", reason: new AiError.ToolParameterValidationError({ toolName: "shell", - toolParams: part.params as Schema.Json, description: error.message }) }) @@ -1199,7 +1193,7 @@ const prepareMessages = Effect.fnUntraced( messages.push({ type: "function_call_output", call_id: part.id, - output: JSON.stringify(part.result), + output: typeof part.result === "string" ? part.result : JSON.stringify(part.result), ...(Predicate.isNotNull(status) ? { status } : {}) }) } @@ -1398,7 +1392,6 @@ const makeResponse = Effect.fnUntraced( method: "makeResponse", reason: new AiError.ToolParameterValidationError({ toolName, - toolParams: {}, description: `Faled to securely JSON parse tool parameters: ${cause}` }) }) @@ -2147,7 +2140,6 @@ const makeStreamResponse = Effect.fnUntraced( method: "makeStreamResponse", reason: new AiError.ToolParameterValidationError({ toolName, - toolParams: {}, description: `Failed securely JSON parse tool parameters: ${cause}` }) }) @@ -2450,7 +2442,6 @@ const makeStreamResponse = Effect.fnUntraced( method: "makeStreamResponse", reason: new AiError.ToolParameterValidationError({ toolName: toolCall.name, - toolParams: {}, description: `Failed securely JSON parse tool parameters: ${cause}` }) }) @@ -3104,7 +3095,6 @@ const normalizeMcpToolCall = Effect.fnUntraced(function* - ).pipe(Effect.mapError((error) => - AiError.make({ - module: "OpenAiLanguageModel", - method: "makeResponse", - reason: new AiError.ToolParameterValidationError({ - toolName, - toolParams, - description: formatIssue(error.issue) - }) - }) - )) + Schema.decodeEffect(codec)(toolParams) as Effect.Effect + ).pipe( + Effect.flatMap((decoded) => + Schema.encodeUnknownEffect(tool.parametersSchema)(decoded) as Effect.Effect + ), + Effect.orElseSucceed(() => toolParams) + ) }) diff --git a/repos/effect/packages/ai/openai/src/OpenAiSchema.ts b/repos/effect/packages/ai/openai/src/OpenAiSchema.ts index 2714c893a1..89015b512c 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiSchema.ts +++ b/repos/effect/packages/ai/openai/src/OpenAiSchema.ts @@ -905,38 +905,38 @@ export type Response = typeof Response.Type const ResponseCreatedEvent = Schema.Struct({ type: Schema.Literal("response.created"), response: Response, - sequence_number: Schema.Int + sequence_number: Schema.optionalKey(Schema.Int) }) const ResponseCompletedEvent = Schema.Struct({ type: Schema.Literal("response.completed"), response: Response, - sequence_number: Schema.Int + sequence_number: Schema.optionalKey(Schema.Int) }) const ResponseIncompleteEvent = Schema.Struct({ type: Schema.Literal("response.incomplete"), response: Response, - sequence_number: Schema.Int + sequence_number: Schema.optionalKey(Schema.Int) }) const ResponseFailedEvent = Schema.Struct({ type: Schema.Literal("response.failed"), response: Response, - sequence_number: Schema.Int + sequence_number: Schema.optionalKey(Schema.Int) }) const ResponseOutputItemAddedEvent = Schema.Struct({ type: Schema.Literal("response.output_item.added"), output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), item: OutputItem }) const ResponseOutputItemDoneEvent = Schema.Struct({ type: Schema.Literal("response.output_item.done"), output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), item: OutputItem }) @@ -946,7 +946,7 @@ const ResponseOutputTextDeltaEvent = Schema.Struct({ output_index: Schema.Int, content_index: Schema.Int, delta: Schema.String, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), logprobs: Schema.optionalKey(Schema.Array(Schema.Unknown)) }) @@ -956,7 +956,7 @@ const ResponseOutputTextAnnotationAddedEvent = Schema.Struct({ output_index: Schema.Int, content_index: Schema.Int, annotation_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), annotation: Annotation }) @@ -965,7 +965,7 @@ const ResponseReasoningSummaryPartAddedEvent = Schema.Struct({ item_id: Schema.String, output_index: Schema.Int, summary_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), part: SummaryTextContent }) @@ -974,7 +974,7 @@ const ResponseReasoningSummaryPartDoneEvent = Schema.Struct({ item_id: Schema.String, output_index: Schema.Int, summary_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), part: SummaryTextContent }) @@ -984,14 +984,14 @@ const ResponseReasoningSummaryTextDeltaEvent = Schema.Struct({ output_index: Schema.Int, summary_index: Schema.Int, delta: Schema.String, - sequence_number: Schema.Int + sequence_number: Schema.optionalKey(Schema.Int) }) const ResponseFunctionCallArgumentsDeltaEvent = Schema.Struct({ type: Schema.Literal("response.function_call_arguments.delta"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), delta: Schema.String }) @@ -999,7 +999,7 @@ const ResponseFunctionCallArgumentsDoneEvent = Schema.Struct({ type: Schema.Literal("response.function_call_arguments.done"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), arguments: Schema.String }) @@ -1007,7 +1007,7 @@ const ResponseCodeInterpreterCallCodeDeltaEvent = Schema.Struct({ type: Schema.Literal("response.code_interpreter_call_code.delta"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), delta: Schema.String }) @@ -1015,7 +1015,7 @@ const ResponseCodeInterpreterCallCodeDoneEvent = Schema.Struct({ type: Schema.Literal("response.code_interpreter_call_code.done"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), code: Schema.String }) @@ -1023,7 +1023,7 @@ const ResponseApplyPatchCallOperationDiffDeltaEvent = Schema.Struct({ type: Schema.Literal("response.apply_patch_call_operation_diff.delta"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), delta: Schema.String }) @@ -1031,7 +1031,7 @@ const ResponseApplyPatchCallOperationDiffDoneEvent = Schema.Struct({ type: Schema.Literal("response.apply_patch_call_operation_diff.done"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), delta: Schema.optionalKey(Schema.String) }) @@ -1039,7 +1039,7 @@ const ResponseImageGenerationCallPartialImageEvent = Schema.Struct({ type: Schema.Literal("response.image_generation_call.partial_image"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), partial_image_b64: Schema.String }) @@ -1048,7 +1048,7 @@ const ResponseErrorEvent = Schema.Struct({ code: Schema.NullOr(Schema.String), message: Schema.String, param: Schema.NullOr(Schema.String), - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), status: Schema.optionalKey(Schema.Int) }) @@ -1060,7 +1060,7 @@ const NestedResponseErrorEvent = Schema.Struct({ message: Schema.String, param: Schema.NullOr(Schema.String) }), - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), status: Schema.optionalKey(Schema.Int) }).pipe( Schema.decodeTo( diff --git a/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts b/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts index d027d86d05..c20107c5c1 100644 --- a/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts +++ b/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts @@ -611,6 +611,138 @@ describe("OpenAiClient", () => { ] })))) + it.effect("accepts response stream events without sequence numbers", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + const [, stream] = yield* client.createResponseStream({ + model: "gpt-4o", + input: "test" + }) + + const events = yield* Stream.runCollect(stream) + const decoded = globalThis.Array.from(events) + + assert.deepStrictEqual(decoded.map((event) => event.type), [ + "response.created", + "response.output_item.added", + "response.content_part.added", + "response.output_text.delta", + "response.reasoning_text.delta", + "response.reasoning_text.done", + "response.content_part.done", + "response.completed" + ]) + assert.deepStrictEqual(decoded[0], { + type: "response.created", + response: { + id: "resp_test123", + object: "response", + model: "gpt-4o-mini", + created_at: 1, + output: [], + error: null, + incomplete_details: null + } + }) + assert.deepStrictEqual(decoded[3], { + type: "response.output_text.delta", + output_index: 0, + item_id: "msg_123", + content_index: 0, + delta: "hello", + sequence_number: 4 + }) + assert.deepStrictEqual([decoded[2], decoded[4], decoded[5], decoded[6]], [ + { + type: "response.content_part.added", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + part: { type: "reasoning_text", text: "" } + }, + { + type: "response.reasoning_text.delta", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + delta: "thinking..." + }, + { + type: "response.reasoning_text.done", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + text: "thinking..." + }, + { + type: "response.content_part.done", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + part: { type: "reasoning_text", text: "thinking..." } + } + ]) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Sse", + events: [ + { + type: "response.created", + response: makeResponseBody({ status: "in_progress" }) + }, + { + type: "response.output_item.added", + output_index: 0, + item: { + id: "msg_123", + type: "message", + role: "assistant", + status: "in_progress", + content: [] + } + }, + { + type: "response.content_part.added", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + part: { type: "reasoning_text", text: "" } + }, + { + type: "response.output_text.delta", + output_index: 0, + item_id: "msg_123", + content_index: 0, + delta: "hello", + sequence_number: 4 + }, + { + type: "response.reasoning_text.delta", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + delta: "thinking..." + }, + { + type: "response.reasoning_text.done", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + text: "thinking..." + }, + { + type: "response.content_part.done", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + part: { type: "reasoning_text", text: "thinking..." } + }, + { + type: "response.completed", + response: makeResponseBody() + } + ] + })))) + it.effect("maps HTTP error before stream starts", () => Effect.gen(function*() { const client = yield* OpenAiClient.OpenAiClient @@ -715,8 +847,8 @@ const makeGeneratedTestLayer = ( const makeConfigTestLayer = (configProvider: ConfigProvider.ConfigProvider) => OpenAiClient.layerConfig({ - apiKey: Config.redacted("MY_API_KEY"), - apiUrl: Config.string("MY_API_URL") + apiKey: Config.Redacted("MY_API_KEY"), + apiUrl: Config.String("MY_API_URL") }).pipe( Layer.provideMerge(HttpClientLayer), Layer.provide(Layer.succeed(MockOpenAiResponse, { response: defaultResponse })), diff --git a/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts b/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts index 19b1e45279..75b0f15ff4 100644 --- a/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts +++ b/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts @@ -1,8 +1,8 @@ import { type Generated, OpenAiClient, OpenAiLanguageModel, OpenAiSchema, OpenAiTool } from "@effect/ai-openai" import { assert, describe, it } from "@effect/vitest" -import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" -import { Array, Context, Effect, Layer, Redacted, Ref, Schema, Stream } from "effect" -import { LanguageModel, Prompt, Response as AiResponse, Tool, Toolkit } from "effect/unstable/ai" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Array, Context, Effect, Layer, Redacted, Ref, Schema, SchemaGetter, Stream } from "effect" +import { type AiError, LanguageModel, Prompt, Response as AiResponse, Tool, Toolkit } from "effect/unstable/ai" import { HttpClient, type HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("OpenAiLanguageModel", () => { @@ -208,6 +208,35 @@ describe("OpenAiLanguageModel", () => { }]) }).pipe(Effect.provide(makeTestLayer()))) + it.effect("handles image strings", () => + Effect.gen(function*() { + const base64 = "iVBORw0KGgo=" + const dataUrl = `data:image/png;base64,${base64}` + const upperCaseDataUrl = `DATA:image/png;base64,${base64}` + const url = "https://example.com/image.png" + + yield* LanguageModel.generateText({ + prompt: Prompt.make([Prompt.userMessage({ + content: [base64, dataUrl, upperCaseDataUrl, url].map((data) => + Prompt.filePart({ mediaType: "image/png", data }) + ) + })]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + assert.deepStrictEqual(body.input, [{ + role: "user", + content: [ + { type: "input_image", image_url: `data:image/png;base64,${base64}`, detail: "auto" }, + { type: "input_image", image_url: dataUrl, detail: "auto" }, + { type: "input_image", image_url: upperCaseDataUrl, detail: "auto" }, + { type: "input_image", image_url: url, detail: "auto" } + ] + }]) + }).pipe(Effect.provide(makeTestLayer()))) + it.effect("handles image with custom detail level", () => Effect.gen(function*() { yield* LanguageModel.generateText({ @@ -505,6 +534,46 @@ describe("OpenAiLanguageModel", () => { strictEqual(toolOutput.output, JSON.stringify({ output: "result" })) }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + it.effect("preserves string tool results", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Use the tool" }, + { + role: "assistant", + content: [ + Prompt.toolCallPart({ + id: "call_text", + name: "TestTool", + params: { input: "test" }, + providerExecuted: false + }) + ] + }, + { + role: "tool", + content: [ + Prompt.toolResultPart({ + id: "call_text", + name: "TestTool", + isFailure: false, + result: "PLAIN_TEXT_SENTINEL\n", + providerExecuted: false + }) + ] + } + ]), + toolkit: TestToolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + const toolOutput = body.input.find((item: any) => item.type === "function_call_output") + + assert.isDefined(toolOutput) + strictEqual(toolOutput.output, "PLAIN_TEXT_SENTINEL\n") + }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + it.effect("emits only the specialized output for apply_patch results", () => Effect.gen(function*() { const toolkit = Toolkit.make(OpenAiTool.ApplyPatch({})) @@ -968,6 +1037,112 @@ describe("OpenAiLanguageModel", () => { ]) )) + it.effect("routes invalid tool call params through failureMode: return without failing the effect", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(ReturnModeTool) + const handlers = toolkit.toLayer({ + ReturnModeTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` }) + }) + + const result = yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(handlers) + ) + + strictEqual(result.toolResults.length, 1) + const toolResult = result.toolResults[0]! + strictEqual(toolResult.isFailure, true) + const failure = toolResult.result as AiError.AiError + strictEqual(failure._tag, "AiError") + strictEqual(failure.reason._tag, "ToolParameterValidationError") + }).pipe( + Effect.provide(makeTestLayer({ + body: { output: [makeFunctionCall("ReturnModeTool", { input: 123 })] } + })) + )) + + it.effect("converts transformed tool call params to the tool's standard encoded form", () => + Effect.gen(function*() { + const received = yield* Ref.make(undefined) + const toolkit = Toolkit.make(TransformParamsTool) + const handlers = toolkit.toLayer({ + TransformParamsTool: ({ input }) => + Ref.set(received, input).pipe( + Effect.as({ output: input * 2 }) + ) + }) + + const result = yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(handlers) + ) + + const toolCall = result.toolCalls[0]! + deepStrictEqual(toolCall.params, { input: "21" }) + strictEqual(yield* Ref.get(received), 21) + strictEqual(result.toolResults[0]!.isFailure, false) + deepStrictEqual(result.toolResults[0]!.result, { output: 42 }) + }).pipe( + Effect.provide(makeTestLayer({ + body: { output: [makeFunctionCall("TransformParamsTool", { input: "21" })] } + })) + )) + + it.effect("provides parameter encoding services to provider normalization", () => + Effect.gen(function*() { + const used = yield* Ref.make(false) + const toolkit = Toolkit.make(AsymmetricParamsTool) + const handlers = toolkit.toLayer({ + AsymmetricParamsTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` }) + }) + + const result = yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(handlers), + Effect.provideService(ParamEncodeService, { use: Ref.set(used, true) }) + ) + + strictEqual(yield* Ref.get(used), true) + strictEqual(result.toolResults[0]!.isFailure, false) + deepStrictEqual(result.toolResults[0]!.result, { output: "processed: hello" }) + }).pipe( + Effect.provide(makeTestLayer({ + body: { output: [makeFunctionCall("AsymmetricParamsTool", { input: "hello" })] } + })) + )) + + it.effect("provides parameter encoding services to provider normalization when tool call resolution is disabled", () => + Effect.gen(function*() { + const used = yield* Ref.make(false) + const toolkit = Toolkit.make(AsymmetricParamsTool) + + const result = yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit, + disableToolCallResolution: true + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provideService(ParamEncodeService, { use: Ref.set(used, true) }) + ) + + strictEqual(yield* Ref.get(used), true) + const toolCall = result.toolCalls[0]! + deepStrictEqual(toolCall.params, { input: "hello" }) + }).pipe( + Effect.provide(makeTestLayer({ + body: { output: [makeFunctionCall("AsymmetricParamsTool", { input: "hello" })] } + })) + )) + it.effect("uses canonical OpenAiMcp name for mcp_call", () => Effect.gen(function*() { const result = yield* LanguageModel.generateText({ @@ -986,6 +1161,7 @@ describe("OpenAiLanguageModel", () => { assert.isDefined(toolResult) if (toolResult?.type === "tool-result") { strictEqual(toolResult.name, "OpenAiMcp") + assertTrue(!toolResult.isFailure, "expected a successful MCP result") strictEqual(toolResult.result.name, "CheckPackage") } }).pipe(Effect.provide(makeTestLayer({ @@ -1373,6 +1549,78 @@ describe("OpenAiLanguageModel", () => { assert.isDefined(toolParamsEnd) })) + it.effect("routes invalid tool call params through failureMode: return without failing the stream", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(ReturnModeTool) + const handlers = toolkit.toLayer({ + ReturnModeTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` }) + }) + + const streamEvents = [ + { + type: "response.created", + sequence_number: 1, + response: makeDefaultResponse({ + id: "resp_invalid_params", + status: "in_progress", + output: [] + }) + }, + { + type: "response.output_item.added", + sequence_number: 2, + output_index: 0, + item: { + type: "function_call", + id: "fc_1", + call_id: "call_1", + name: "ReturnModeTool", + arguments: "", + status: "in_progress" + } + }, + { + type: "response.function_call_arguments.done", + sequence_number: 3, + output_index: 0, + item_id: "fc_1", + name: "ReturnModeTool", + arguments: "{\"input\":123}" + }, + { + type: "response.completed", + sequence_number: 4, + response: makeDefaultResponse({ + id: "resp_invalid_params", + status: "completed", + output: [] + }) + } + ] as unknown as ReadonlyArray + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "Use the test tool", + toolkit + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeStreamTestLayer(streamEvents)), + Effect.provide(handlers) + ) + + const parts = globalThis.Array.from(partsChunk) + const toolResults = parts.filter((part) => part.type === "tool-result") + strictEqual(toolResults.length, 1) + const toolResult = toolResults[0] + if (toolResult?.type === "tool-result") { + strictEqual(toolResult.isFailure, true) + const result = toolResult.result as AiError.AiError + strictEqual(result._tag, "AiError") + strictEqual(result.reason._tag, "ToolParameterValidationError") + } + assert.isDefined(parts.find((part) => part.type === "finish")) + })) + it.effect("waits for the stable streamed web search action before emitting the tool call", () => Effect.gen(function*() { const toolkit = Toolkit.make(OpenAiTool.WebSearch({})) @@ -1540,6 +1788,7 @@ describe("OpenAiLanguageModel", () => { assert.isDefined(toolResult) if (toolResult?.type === "tool-result") { strictEqual(toolResult.name, "OpenAiMcp") + assertTrue(!toolResult.isFailure, "expected a successful MCP result") strictEqual(toolResult.result.name, "CheckPackage") } })) @@ -1912,6 +2161,42 @@ const TestTool = Tool.make("TestTool", { success: Schema.Struct({ output: Schema.String }) }) +const ReturnModeTool = Tool.make("ReturnModeTool", { + description: "A test tool", + failureMode: "return", + parameters: Schema.Struct({ input: Schema.String }), + success: Schema.Struct({ output: Schema.String }), + failure: Schema.Struct({ error: Schema.String }) +}) + +const TransformParamsTool = Tool.make("TransformParamsTool", { + description: "A test tool", + parameters: Schema.Struct({ input: Schema.FiniteFromString }), + success: Schema.Struct({ output: Schema.Finite }) +}) + +class ParamEncodeService extends Context.Service +}>()("ParamEncodeService") {} + +const AsymmetricParam = Schema.String.pipe( + Schema.decodeTo(Schema.String, { + decode: SchemaGetter.passthrough(), + encode: SchemaGetter.transformOrFail((value) => + Effect.service(ParamEncodeService).pipe( + Effect.flatMap((service) => service.use), + Effect.as(value) + ) + ) + }) +) + +const AsymmetricParamsTool = Tool.make("AsymmetricParamsTool", { + description: "A test tool", + parameters: Schema.Struct({ input: AsymmetricParam }), + success: Schema.Struct({ output: Schema.String }) +}) + const TestToolkit = Toolkit.make(TestTool) const McpToolkit = Toolkit.make(OpenAiTool.Mcp({ diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts index b32664cd9d..eb5e9a60f8 100644 --- a/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts @@ -557,8 +557,9 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig const messages = yield* prepareMessages({ options }) const { tools, toolChoice } = yield* prepareTools({ options, transformer: codecTransformer }) const responseFormat = yield* getResponseFormat({ config, options, transformer: codecTransformer }) + const { strictJsonSchema: _sjs, ...apiConfig } = config const request: typeof Generated.ChatRequest.Encoded = { - ...config, + ...apiConfig, messages, ...(Predicate.isNotUndefined(responseFormat) ? { response_format: responseFormat } : undefined), ...(Predicate.isNotUndefined(tools) ? { tools } : undefined), @@ -899,7 +900,7 @@ const prepareMessages = Effect.fnUntraced( messages.push({ role: "tool", tool_call_id: part.id, - content: JSON.stringify(part.result) + content: typeof part.result === "string" ? part.result : JSON.stringify(part.result) }) } @@ -1046,7 +1047,6 @@ const makeResponse = Effect.fnUntraced( method: "makeResponse", reason: new AiError.ToolParameterValidationError({ toolName, - toolParams: {}, description: `Failed to securely JSON parse tool parameters: ${cause}` }) }) @@ -1495,7 +1495,7 @@ const makeStreamResponse = Effect.fnUntraced( (detail) => detail.type === "reasoning.encrypted" && detail.data.length > 0 ) if (totalToolCalls > 0 && hasEncryptedReasoning && finishReason === "stop") { - finishReason = resolveFinishReason("tool-calls") + finishReason = "tool-calls" } // Forward any unsent tool calls if finish reason is 'tool-calls' diff --git a/repos/effect/packages/ai/openrouter/test/Generated.test.ts b/repos/effect/packages/ai/openrouter/test/Generated.test.ts index 8790fed735..0db46d615b 100644 --- a/repos/effect/packages/ai/openrouter/test/Generated.test.ts +++ b/repos/effect/packages/ai/openrouter/test/Generated.test.ts @@ -1,7 +1,9 @@ -import { Generated } from "@effect/ai-openrouter" +import { Generated, OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter" import { describe, it } from "@effect/vitest" import { deepStrictEqual } from "@effect/vitest/utils" -import { Schema } from "effect" +import { Effect, Schema, Stream } from "effect" +import { type AiError, LanguageModel, type Response, Tool, Toolkit } from "effect/unstable/ai" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("Generated", () => { it("decodes nullable generation statistics", () => { @@ -72,4 +74,85 @@ describe("Generated", () => { deepStrictEqual(Schema.decodeUnknownSync(Generated.ChatUsage)(usage), usage) }) + + for ( + const { encrypted, mode, reason, tool } of [ + { mode: "stream", encrypted: true, tool: true, reason: "tool-calls" }, + { mode: "stream", encrypted: false, tool: true, reason: "stop" }, + { mode: "stream", encrypted: true, tool: false, reason: "stop" }, + { mode: "generate", encrypted: true, tool: true, reason: "tool-calls" } + ] as const + ) { + it.effect(`${mode}: encrypted=${encrypted}, tool=${tool} finishes with ${reason}`, () => + Effect.gen(function*() { + const metadata = { id: "response-1", model: "test/reasoning-model", created: 1 } + const reasoningDetails = encrypted + ? [{ type: "reasoning.encrypted", data: "opaque-signature", format: "unknown" } as const] + : [] + const toolCalls = tool + ? [{ + index: 0, + id: "call-1", + type: "function" as const, + function: { name: "ProbeTool", arguments: "{\"value\":1}" } + }] + : [] + const usage = { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } + const chunks = yield* Schema.decodeUnknownEffect(Schema.Array(Generated.ChatStreamChunk))([ + { + ...metadata, + object: "chat.completion.chunk", + choices: [{ index: 0, delta: { reasoning_details: reasoningDetails, tool_calls: toolCalls } }] + }, + { + ...metadata, + object: "chat.completion.chunk", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + usage + } + ]) + const body = yield* Schema.decodeUnknownEffect(Generated.SendChatCompletionRequest200)({ + ...metadata, + object: "chat.completion", + system_fingerprint: null, + choices: [{ + index: 0, + finish_reason: "stop", + message: { role: "assistant", reasoning_details: reasoningDetails, tool_calls: toolCalls } + }], + usage + }) + const response = HttpClientResponse.fromWeb( + HttpClientRequest.post("https://example.com/chat/completions"), + new globalThis.Response() + ) + const toolkit = Toolkit.make(Tool.make("ProbeTool", { + parameters: Schema.Struct({ value: Schema.Number }), + success: Schema.String + })) + const client = OpenRouterClient.OpenRouterClient.of({ + client: Generated.make(HttpClient.make(() => Effect.die("Unexpected HTTP request"))), + createChatCompletion: () => Effect.succeed([body, response]), + createChatCompletionStream: () => Effect.succeed([response, Stream.fromIterable(chunks)]) + }) + const options = { prompt: "Use the probe tool", toolkit, disableToolCallResolution: true } as const + const operation: Effect.Effect, AiError.AiError, LanguageModel.LanguageModel> = + mode === "stream" + ? LanguageModel.streamText(options).pipe(Stream.runCollect) + : LanguageModel.generateText(options).pipe(Effect.map((result) => result.content)) + const parts = globalThis.Array.from( + yield* operation.pipe( + Effect.provide(OpenRouterLanguageModel.model(metadata.model)), + Effect.provide(toolkit.toLayer({ ProbeTool: () => Effect.succeed("ok") })), + Effect.provideService(OpenRouterClient.OpenRouterClient, client) + ) + ) + + deepStrictEqual( + parts.filter((part) => part.type === "tool-call").map((part) => part.name), + tool ? ["ProbeTool"] : [] + ) + deepStrictEqual(parts.filter((part) => part.type === "finish").map((part) => part.reason), [reason]) + })) + } }) diff --git a/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts b/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts index ab6738fdda..7874a82961 100644 --- a/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts +++ b/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts @@ -6,6 +6,49 @@ import { LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai" import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("OpenRouterLanguageModel", () => { + describe("strictJsonSchema", () => { + it.effect("omits false from requests while preserving response strictness", () => + Effect.gen(function*() { + yield* LanguageModel.generateObject({ + prompt: "Give me a name", + schema: Schema.Struct({ name: Schema.String }) + }).pipe(Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini", { strictJsonSchema: false }))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + strictEqual(body.response_format.json_schema.strict, false) + assert.notProperty(body, "strictJsonSchema") + }).pipe(Effect.provide(makeTestLayer({ + body: { + choices: [{ + finish_reason: "stop", + index: 0, + message: { role: "assistant", content: JSON.stringify({ name: "Alice" }) } + }] + } + })))) + + it.effect("omits true from streaming requests while preserving tool strictness", () => + Effect.gen(function*() { + const tool = Tool.make("FlexibleTool", { parameters: Schema.Struct({ query: Schema.String }) }) + .annotate(Tool.Strict, false) + yield* LanguageModel.streamText({ + prompt: "Use a tool", + toolkit: Toolkit.make(tool), + disableToolCallResolution: true + }).pipe( + Stream.runDrain, + Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini", { strictJsonSchema: true })) + ) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + strictEqual(body.stream, true) + strictEqual(body.tools[0].function.strict, false) + assert.notProperty(body, "strictJsonSchema") + }).pipe(Effect.provide(makeStreamTestLayer([])))) + }) + describe("generateText", () => { describe("message preparation", () => { describe("audio file parts", () => { @@ -171,6 +214,42 @@ describe("OpenRouterLanguageModel", () => { }]) }).pipe(Effect.provide(makeTestLayer()))) }) + + it.effect("preserves string tool results", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Use the tool" }, + { + role: "assistant", + content: [Prompt.toolCallPart({ + id: "call_text", + name: "text_tool", + params: {}, + providerExecuted: false + })] + }, + { + role: "tool", + content: [Prompt.toolResultPart({ + id: "call_text", + name: "text_tool", + result: "PLAIN_TEXT_SENTINEL\n", + isFailure: false, + providerExecuted: false + })] + } + ]), + disableToolCallResolution: true + }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + const toolResult = body.messages.find((message: any) => message.role === "tool") + + assert.isDefined(toolResult) + strictEqual(toolResult.content, "PLAIN_TEXT_SENTINEL\n") + }).pipe(Effect.provide(makeTestLayer()))) }) describe("tool preparation", () => { @@ -439,20 +518,27 @@ const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => const makeStreamTestLayer = (events: ReadonlyArray) => { const body = events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") + "data: [DONE]\n\n" - const httpClient = HttpClient.makeWith( - Effect.fnUntraced(function*(requestEffect) { - const request = yield* requestEffect - return HttpClientResponse.fromWeb( - request, - new Response(body, { - status: 200, - headers: { "content-type": "text/event-stream" } - }) - ) - }), - Effect.succeed as HttpClient.HttpClient.Preprocess - ) + const httpClientLayer = Layer.effectContext(Effect.gen(function*() { + const capturedRequests = yield* Ref.make>([]) + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + yield* Ref.update(capturedRequests, Array.append(request)) + return HttpClientResponse.fromWeb( + request, + new Response(body, { + status: 200, + headers: { "content-type": "text/event-stream" } + }) + ) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + return Context.make(HttpClient.HttpClient, httpClient).pipe( + Context.add(MockHttpClient, MockHttpClient.of({ requests: Ref.get(capturedRequests) })) + ) + })) return OpenRouterClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( - Layer.provide(Layer.succeed(HttpClient.HttpClient, httpClient)) + Layer.provideMerge(httpClientLayer) ) } diff --git a/repos/effect/packages/atom/react/package.json b/repos/effect/packages/atom/react/package.json index f838a7d6fa..cffc768ce2 100644 --- a/repos/effect/packages/atom/react/package.json +++ b/repos/effect/packages/atom/react/package.json @@ -67,15 +67,15 @@ "devDependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.1", - "@testing-library/react": "^16.3.2", + "@testing-library/react": "^16.3.3", "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", + "@types/react-dom": "^19.2.7", "@types/scheduler": "^0.26.0", "effect": "workspace:^", "jsdom": "^30.0.1", "react": "^19.2.8", "react-dom": "^19.2.8", - "react-error-boundary": "^6.1.3", + "react-error-boundary": "^6.1.5", "scheduler": "^0.27.0" } } diff --git a/repos/effect/packages/atom/react/src/Hooks.ts b/repos/effect/packages/atom/react/src/Hooks.ts index 4632ea3333..5fe03b33bc 100644 --- a/repos/effect/packages/atom/react/src/Hooks.ts +++ b/repos/effect/packages/atom/react/src/Hooks.ts @@ -435,8 +435,8 @@ export const useAtomSubscribe = ( * @since 4.0.0 */ export const useAtomRef = (ref: AtomRef.ReadonlyRef): A => { - const [, setValue] = React.useState(ref.value) - React.useEffect(() => ref.subscribe(setValue), [ref]) + const [, forceUpdate] = React.useReducer((n) => n + 1, 0) + React.useEffect(() => ref.subscribe(forceUpdate), [ref]) return ref.value } diff --git a/repos/effect/packages/atom/react/test/index.test.tsx b/repos/effect/packages/atom/react/test/index.test.tsx index 06c2668b95..f00e71a6f7 100644 --- a/repos/effect/packages/atom/react/test/index.test.tsx +++ b/repos/effect/packages/atom/react/test/index.test.tsx @@ -4,6 +4,7 @@ import { Cause, Context, Effect, Latch, Layer } from "effect" import * as Schema from "effect/Schema" import * as AsyncResult from "effect/unstable/reactivity/AsyncResult" import * as Atom from "effect/unstable/reactivity/Atom" +import * as AtomRef from "effect/unstable/reactivity/AtomRef" import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" import * as Hydration from "effect/unstable/reactivity/Hydration" import * as React from "react" @@ -11,10 +12,18 @@ import { Suspense } from "react" import { renderToString } from "react-dom/server" import { ErrorBoundary } from "react-error-boundary" import { beforeEach, describe, expect, it, test, vi } from "vitest" -import { HydrationBoundary, RegistryContext, RegistryProvider, useAtomSuspense, useAtomValue } from "../src/index.ts" +import { + HydrationBoundary, + RegistryContext, + RegistryProvider, + useAtomRef, + useAtomSuspense, + useAtomValue +} from "../src/index.ts" import * as ScopedAtom from "../src/ScopedAtom.ts" -describe("atom-react", () => { +// Tests share the DOM and registry. +describe("atom-react", { concurrent: false }, () => { let registry: AtomRegistry.AtomRegistry beforeEach(() => { @@ -180,6 +189,24 @@ describe("atom-react", () => { }) }) + test("useAtomRef updates after switching refs", () => { + const first = AtomRef.make(0) + const second = AtomRef.make(1) + + function TestComponent({ source }: { readonly source: AtomRef.ReadonlyRef }) { + return
{useAtomRef(source)}
+ } + + const { rerender } = render() + rerender() + + act(() => { + second.set(0) + }) + + expect(screen.getByTestId("value")).toHaveTextContent("0") + }) + describe("ScopedAtom", () => { test("throws when used outside Provider", () => { const counter = ScopedAtom.make(() => Atom.make(0)) diff --git a/repos/effect/packages/atom/solid/src/RegistryContext.ts b/repos/effect/packages/atom/solid/src/RegistryContext.ts index 7c1b6c0f2e..111cfab28e 100644 --- a/repos/effect/packages/atom/solid/src/RegistryContext.ts +++ b/repos/effect/packages/atom/solid/src/RegistryContext.ts @@ -68,7 +68,7 @@ export const RegistryProvider = (options: { scheduleTask: options.scheduleTask, initialValues: options.initialValues, timeoutResolution: options.timeoutResolution, - defaultIdleTTL: options.defaultIdleTTL ?? 400 + defaultIdleTTL: options.defaultIdleTTL }) onCleanup(() => registry.dispose()) return createComponent(RegistryContext.Provider, { diff --git a/repos/effect/packages/atom/vue/package.json b/repos/effect/packages/atom/vue/package.json index 20c039cc1d..97d57eaab0 100644 --- a/repos/effect/packages/atom/vue/package.json +++ b/repos/effect/packages/atom/vue/package.json @@ -61,7 +61,7 @@ }, "devDependencies": { "effect": "workspace:^", - "vue": "^3.5.41" + "vue": "^3.5.42" }, "peerDependencies": { "effect": "workspace:^", diff --git a/repos/effect/packages/atom/vue/src/index.ts b/repos/effect/packages/atom/vue/src/index.ts index a2f4340074..b4fff5a91a 100644 --- a/repos/effect/packages/atom/vue/src/index.ts +++ b/repos/effect/packages/atom/vue/src/index.ts @@ -206,6 +206,7 @@ export const useAtomRef =
(atomRef: () => AtomRef.ReadonlyRef): Readonly { value.value = next })) + value.value = ref.value }) return value as Readonly> } diff --git a/repos/effect/packages/atom/vue/test/index.test.ts b/repos/effect/packages/atom/vue/test/index.test.ts index 79cf1df55c..ae0b2f8856 100644 --- a/repos/effect/packages/atom/vue/test/index.test.ts +++ b/repos/effect/packages/atom/vue/test/index.test.ts @@ -1,5 +1,24 @@ -import { describe, test } from "vitest" +import { useAtomRef } from "@effect/atom-vue" +import { assert, describe, it } from "@effect/vitest" +import * as AtomRef from "effect/unstable/reactivity/AtomRef" +import { effectScope, nextTick, shallowRef } from "vue" describe("atom-vue", () => { - test("", () => {}) + describe("useAtomRef", () => { + it("publishes the current value when the selected ref changes", async () => { + const first = AtomRef.make(1) + const second = AtomRef.make(10) + const selected = shallowRef(first) + const scope = effectScope() + try { + const value = scope.run(() => useAtomRef(() => selected.value))! + + selected.value = second + await nextTick() + assert.strictEqual(value.value, 10) + } finally { + scope.stop() + } + }) + }) }) diff --git a/repos/effect/packages/effect/ARBITRARY-FOLLOW-UPS.md b/repos/effect/packages/effect/ARBITRARY-FOLLOW-UPS.md new file mode 100644 index 0000000000..862a47e3a0 --- /dev/null +++ b/repos/effect/packages/effect/ARBITRARY-FOLLOW-UPS.md @@ -0,0 +1,62 @@ +# Native Arbitrary Follow-ups + +This file tracks unfinished work for the native Schema-first Arbitrary implementation. Settled behavior and technical +decisions belong in [ARBITRARY.md](ARBITRARY.md); migration guidance belongs in +[ARBITRARY-MIGRATION.md](ARBITRARY-MIGRATION.md). Remove an item from this file when it is resolved rather than keeping +completed implementation history here. + +Every production change must preserve the existing guarantees: + +- Schema remains the only catalog of primitive and structural generator constructors; +- discarded roots are bounded by `maxDiscards`; +- inspected shrink candidates, including rejected nodes, are bounded by `maxShrinks`; +- shrinking and replay remain deterministic for supported pure callbacks; +- recursive and mutually recursive Schemas retain their productivity guarantees; +- `Sample`, the shrink carrier, the PRNG, generation budgets, and compiler metadata remain private; +- runtime performance and bundle cost are measured before and after the change. + +## Conditional research + +These items are intentionally dormant until their trigger is observed. + +### Finite-domain metadata + +Evaluate private finite-domain metadata only if constructive unique generation demonstrates a real exhaustion or +productivity problem. Do not add public cardinality vocabulary preemptively. + +### Decoded collection and Declaration profiling + +Profile collection generation Links and Declaration decoding only when a new runtime baseline identifies a regression. +ReadonlyMap and ReadonlySet are compiler-owned; Effect-specific HashMap, HashSet, and Chunk keep declaration-local +generation Links. + +### Schema-scoped distribution customization + +Revisit application-owned distribution overrides, including deterministic Faker integration, only after a concrete +use case establishes the required scope and bundle boundary. A future design must work for checked and nested Schemas, +must not overload filter or declaration annotations with a second contract, and must not require synthetic Schemas in +test integrations. Prefer a derivation-time override mechanism over executable metadata captured by production Schema +modules. + +### Trace-informed shrinking + +Compare the current `Sample` tree with private structural spans or trace-informed shrinking only when a reproducible +case shows poor shrunk output or excessive candidate traversal. + +### Concrete failing-input persistence + +Evaluate persistence and reuse of concrete failing inputs last. After `map` or `flatMap`, an Arbitrary may no longer +have a Schema or codec capable of serializing its output, while the existing opaque replay token remains persistable. + +## Verification policy + +For every activated item, use the narrowest representative validation and record exact commands and artifacts: + +- focused Arbitrary runtime tests and typetests when the public types change; +- package type checking and linting; +- seeded generation, shrinking, and replay characterization relevant to the slice; +- focused warm and cold runtime scenarios; +- focused Arbitrary bundle fixtures plus production Schema bundle sentinels. + +Do not update public JSDoc, [ARBITRARY.md](ARBITRARY.md), the migration guide, or the changeset until the corresponding +behavior has passed its semantic, runtime, and bundle gates. diff --git a/repos/effect/packages/effect/ARBITRARY-MIGRATION.md b/repos/effect/packages/effect/ARBITRARY-MIGRATION.md new file mode 100644 index 0000000000..c40815c2db --- /dev/null +++ b/repos/effect/packages/effect/ARBITRARY-MIGRATION.md @@ -0,0 +1,335 @@ +# Migrating to Native Arbitrary + +This guide covers migration from the fast-check bridge published in `effect@4.0.0-rc.109` to the native, +Schema-first module at `effect/unstable/arbitrary`. + +The new module removes fast-check from the `effect` package. Applications may still install and use fast-check +directly, but Effect Schema generation and `@effect/vitest` property tests no longer depend on it. + +For the new API and its semantics, see [Arbitrary in Effect](ARBITRARY.md). + +## Import Changes + +The following APIs have been removed: + +- `effect/testing/FastCheck`; +- `Schema.toArbitrary`; +- `Schema.Arbitrary`; +- the legacy `Schema.Annotations.ToArbitrary` contract and declaration-level `toArbitrary` annotation; +- the legacy `arbitrary` filter annotation; +- raw fast-check arbitrary inputs and `fastCheck` options in `@effect/vitest`. + +Import the native module explicitly: + +```ts +import { Arbitrary } from "effect/unstable/arbitrary" +``` + +If other tests still use fast-check-specific APIs, add fast-check as a direct development dependency and import it +from `"fast-check"`. Do not import it through Effect. + +## Generating Samples + +Previously, `Schema.toArbitrary` returned a factory that needed the fast-check module: + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +const Person = Schema.Struct({ + name: Schema.String, + age: Schema.Int +}) + +const personArbitrary = Schema.toArbitrary(Person)(FastCheck) +const samples = FastCheck.sample(personArbitrary, { numRuns: 20, seed: 42 }) +``` + +Now derive and sample through the Effect-native module: + +```ts +import { Effect, Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const Person = Schema.Struct({ + name: Schema.String, + age: Schema.Int +}) + +const personArbitrary = Arbitrary.schema(Person) +const samples = await Effect.runPromise( + Arbitrary.sampleEffect(personArbitrary, { count: 20, seed: 42 }) +) +``` + +`Arbitrary.sampleEffect` returns an `Effect` because sampling is interruptible, uses Effect `Random` when no seed is +provided, and reports bounded generation exhaustion as a typed `SampleError`. + +The generated values still use the decoded Schema `Type`. The sequence and distribution are not compatible with +fast-check, even when the same numeric seed is used. + +## Checking Properties + +Previously, fast-check owned both the property and the runner: + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +const integer = Schema.toArbitrary(Schema.Int)(FastCheck) + +FastCheck.assert( + FastCheck.property(integer, (value) => Number.isInteger(value)), + { numRuns: 100, seed: 42 } +) +``` + +Now `Arbitrary.checkEffect` runs a pure or Effectful property and returns a structured result: + +```ts +import { Effect, Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const result = await Effect.runPromise( + Arbitrary.checkEffect( + Arbitrary.schema(Schema.Int), + (value) => Number.isInteger(value), + { runs: 100, seed: 42 } + ) +) +``` + +Unlike `FastCheck.assert`, `Arbitrary.checkEffect` does not throw for an ordinary falsification. Handle `Passed`, +`Falsified`, `Exhausted`, and `ReplayMismatch` explicitly, or use `@effect/vitest`, which converts non-passing results +into test failures. + +Typed failures from Effectful properties are preserved in `Falsified.failure`. Defects and interruption continue +through the returned Effect. + +## Option Mapping + +The most common options map as follows: + +| Previous fast-check option | Native option | Migration note | +| -------------------------- | -------------------- | ------------------------------------------------------------------ | +| `numRuns` | `count` or `runs` | Use `count` for `sampleEffect` and `runs` for `checkEffect`. | +| `seed` | `seed` | The type is compatible, but generated sequences are not. | +| `path` | `replay` | Existing fast-check paths cannot be converted. | +| `maxSkipsPerRun` | `maxDiscards` | Native uses one absolute discard budget, not a multiplier per run. | +| `examples` | No direct equivalent | Keep explicit regression cases as ordinary tests. | +| `endOnFailure` | `maxShrinks` | Use `maxShrinks: 0` to stop at the initial failure. | +| `interruptAfterTimeLimit` | Effect interruption | Apply an Effect or test timeout around the check. | +| `skipAllAfterTimeLimit` | No direct equivalent | Prefer explicit run and discard bounds. | +| `verbose` | No direct equivalent | Inspect `CheckResult` or use `@effect/vitest` failure output. | + +Review any less common fast-check runner option manually. The native API deliberately does not reproduce the complete +`fc.Parameters` surface. + +## Replay Migration + +Fast-check replay used a seed plus a shrink `path`. Native replay uses one opaque token returned by a `Falsified` +result: + +```ts +const replayed = Arbitrary.checkEffect(arbitrary, property, { + replay: previousFailure.replay +}) +``` + +There is no conversion from a fast-check seed and path to a native replay token. Re-run the property with the native +engine, then record the new token from its `Falsified` result. + +Replay tokens are intended for reproducing and diagnosing a current failure. Because the module is unstable, they are +not guaranteed to survive upgrades. Preserve important failing inputs as explicit regression tests. + +## Migrating Declaration Annotations + +The old `toArbitrary` annotation directly constructed a fast-check arbitrary and exposed fast-check recursion and +constraint details: + +```ts +import { Schema } from "effect" + +class UserId { + readonly value: number + constructor(value: number) { + this.value = value + } +} + +const UserIdSchema = Schema.instanceOf(UserId, { + toArbitrary: () => (fc) => fc.integer({ min: 1, max: 1_000_000 }).map((value) => new UserId(value)) +}) +``` + +The native `toCodecArbitrary` annotation describes a generatable representation as a Schema `Link`: + +```ts +import { Schema, SchemaTransformation } from "effect" + +class UserId { + readonly value: number + constructor(value: number) { + this.value = value + } +} + +const UserIdSchema = Schema.instanceOf(UserId, { + toCodecArbitrary: () => + Schema.link()( + Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000_000 })), + SchemaTransformation.transform({ + decode: (value) => new UserId(value), + encode: (id) => id.value + }) + ) +}) +``` + +Before adding `toCodecArbitrary`, check whether the declaration already has a useful `toCodecJson` or `toCodec`. +Native derivation falls back to those canonical codecs automatically. Add an arbitrary-specific Link only when the +canonical representation is opaque or generates valid values too rarely. + +The migration changes where generation logic lives: + +| Legacy contract | Native contract | +| ---------------------------------------------- | -------------------------------------------------------------- | +| Returns a `fast-check.Arbitrary` | Returns a `SchemaAST.Link` through `Schema.link`. | +| Receives generated arbitrary type parameters | Receives decoded Schema type parameters. | +| Receives fast-check constraints and recursion | Receives normalized constraints. | +| Manages terminal recursive branches explicitly | Leaves recursion analysis and budgets to the native compiler. | +| Uses arbitrary combinators | Uses Schema constructors, checks, and a Schema transformation. | + +The original declaration remains authoritative. Values decoded by the Link are checked against it. Failed decodes and +rejected values become bounded discards. + +### Custom Filter Metadata + +The old `arbitrary` filter annotation has been replaced by `arbitraryConstraint`. Ordinary custom filters continue to +work as residual filters without generation metadata: + +```ts +import { Schema } from "effect" + +const Even = Schema.Int.check( + Schema.makeFilter((value) => value % 2 === 0) +) +``` + +Residual filtering is bounded, so a very selective or impossible predicate may produce `SampleError` or `Exhausted`. + +If the previous annotation supplied a recognized constructive constraint, move it to `arbitraryConstraint` and adapt +its shape. The predicate remains authoritative: + +```ts +import { Order, Schema } from "effect" + +const Positive = Schema.Number.check( + Schema.makeFilter( + (value) => value > 0, + { + arbitraryConstraint: { + order: Order.Number, + minimum: 0, + exclusiveMinimum: true + } + } + ) +) +``` + +The main constraint-shape changes are: + +| Previous field | Native field | +| ------------------------------------ | ------------------------------------------------------------------- | +| `ordered.order` | `order` | +| `ordered.minimum` / `maximum` | `minimum` / `maximum` | +| `ordered.exclusiveMinimum` / maximum | `exclusiveMinimum: true` / `exclusiveMaximum: true` | +| `integer: true` | `number: "integer"` | +| `noNaN` and `noInfinity` | `number: "finite"` when both restrictions apply | +| collection `minLength` / `maxLength` | `minLength`, `minSize`, or `minProperties` and its matching maximum | +| string pattern | `{ source, flags }` in `patterns` | +| `unique: true` | `uniqueBy: identity` | +| `candidate` | No direct equivalent | + +Choose the cardinality field that matches the Schema domain: `minLength` and `maxLength` for strings and arrays, +`minSize` and `maxSize` for sized collections, and `minProperties` and `maxProperties` for object properties. + +For an opaque declaration that needs a reusable statistically better source domain, express that source as a Schema +Link with `toCodecArbitrary`. + +## Migrating `@effect/vitest` + +Property inputs may be Schemas, native Arbitraries, or mixtures of both. + +Schema-only properties need only an option rename: + +```ts +// Before +it.prop( + "commutative", + [Schema.Int, Schema.Int], + ([a, b]) => a + b === b + a, + { fastCheck: { numRuns: 200, seed: 42 } } +) + +// After +it.prop( + "commutative", + [Schema.Int, Schema.Int], + ([a, b]) => a + b === b + a, + { arbitrary: { runs: 200, seed: 42 } } +) +``` + +Raw or mixed fast-check inputs are no longer accepted: + +```ts +// No longer supported +it.prop("raw arbitrary", [fc.integer()], ([value]) => Number.isInteger(value)) +it.prop("mixed", [Schema.String, fc.integer()], ([text, value]) => true) +``` + +Replace those inputs with Schemas when they describe a domain supported by Schema, or compose a native Arbitrary: + +```ts +import { Arbitrary } from "effect/unstable/arbitrary" + +const integer = Arbitrary.schema(Schema.Int) + +it.prop("native arbitrary", [integer], ([value]) => Number.isInteger(value)) +it.prop("mixed", [Schema.String, integer], ([text, value]) => typeof text === "string" && Number.isInteger(value)) +``` + +If a test genuinely needs a fast-check-specific arbitrary or runner feature, use fast-check directly with Vitest +rather than passing it through `@effect/vitest`. + +`it.prop`, `it.effect.prop`, and `it.live.prop` all accept native check options under `arbitrary`. + +## Behavioral Differences to Review + +Migration is not only an import rename. Review the following differences: + +- native generation and shrinking have different distributions and may find different shrunk inputs; +- native checking returns structured results instead of using fast-check's assertion exceptions; +- generation that cannot find enough valid samples is bounded and reports `SampleError` or `Exhausted`; +- pure and Effectful properties share one interruptible runner; +- recursive and mutually recursive Schemas are analyzed as a graph and must have a finite generation path; +- replay tokens, seeds, and shrink paths are not compatible with fast-check; +- generated values are the decoded Schema `Type`; +- properties must not mutate generated values. + +## Migration Checklist + +1. Replace `effect/testing/FastCheck` imports. Use the native Arbitrary module for Schema generation and import + `"fast-check"` directly only where it is still independently required. +2. Replace `Schema.toArbitrary(schema)(FastCheck)` with `Arbitrary.schema(schema)`. +3. Replace `FastCheck.sample` with `Arbitrary.sampleEffect` and run the returned Effect. +4. Replace `FastCheck.check` or `FastCheck.assert` for Schema-derived inputs with `Arbitrary.checkEffect`, then handle its + structured result. +5. Rename `@effect/vitest` options from `fastCheck` to `arbitrary` and convert `numRuns` to `runs`. +6. Replace raw fast-check inputs in `@effect/vitest` with Schemas or native Arbitraries. +7. Migrate declaration-level `toArbitrary` callbacks to the `toCodecArbitrary` Link-returning contract and replace old + filter-level `arbitrary` annotations with `arbitraryConstraint`. +8. Re-run properties with the native engine and record new replay tokens or explicit regression examples. +9. Review discard limits for selective custom filters. diff --git a/repos/effect/packages/effect/ARBITRARY.md b/repos/effect/packages/effect/ARBITRARY.md new file mode 100644 index 0000000000..9c77780243 --- /dev/null +++ b/repos/effect/packages/effect/ARBITRARY.md @@ -0,0 +1,1300 @@ +# From Examples to Laws: Property-Based Testing with Effect's `Arbitrary` + +Most unit tests choose a few inputs by hand and check the expected result for each one. Property-based testing asks the +computer to try many inputs for us. + +Instead of listing every expected answer, we write a **property**: a rule that should be true for every allowed input. +If the rule fails, the test tries simpler versions of the failing input. This simplification step is called +**shrinking**, and the failing input reported to the user is called a **counterexample**. + +Effect divides the work into three parts: + +- `Schema` describes which inputs are allowed. +- `Arbitrary` describes how to produce values of type `A` and how to simplify them after a failure. You can think of + it as an input generator with built-in shrinking. +- `it.prop`, `it.effect.prop`, or `Arbitrary.checkEffect` runs the rule against generated inputs. + +The API used here is currently available from `effect/unstable/arbitrary`. The `unstable` segment matters: +the ideas are stable, but names, result types, generation policies, and replay format may still change before this +module is promoted. + +If you are upgrading from the earlier Schema arbitrary integration available in `effect@4.0.0-rc.109`, see the +[migration guide](ARBITRARY-MIGRATION.md). + +## Writing a First Property + +Consider the rule “adding zero does not change an integer.” With `@effect/vitest`, we can write it directly: + +```ts +import { it } from "@effect/vitest" +import { Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const integer = Arbitrary.schema(Schema.Int) + +it.prop("adding zero is identity", [integer], ([value]) => value + 0 === value) +``` + +`Arbitrary.schema` turns a Schema into a generator for the values represented by that Schema. `it.prop` tries the rule +100 times by default. It starts with small values, gradually tries more complex ones, and simplifies the first failing +input it finds. + +Effect keeps the allowed inputs visible. Even a one-argument property receives its inputs from an array or record: + +```ts +it.prop( + "addition is commutative", + { left: integer, right: integer }, + ({ left, right }) => left + right === right + left +) +``` + +That explicitness becomes useful as a test grows. Input names remain visible, and each input may come from either a +Schema or a pre-built `Arbitrary`. + +## Choosing the Inputs + +The rule and its allowed inputs belong together. A rule can be mathematically correct and still be unsuitable for the +values used by the program. + +For example, JavaScript cannot represent integers of every size exactly. If we want to test ordinary integer arithmetic +without overflow or loss of precision, we should restrict the inputs accordingly: + +```ts +import { Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const SmallInt = Schema.Int.check( + Schema.isBetween({ minimum: -100, maximum: 100 }) +) + +const smallInt = Arbitrary.schema(SmallInt) +``` + +For common checks, such as numeric bounds and collection lengths, Effect generates matching values directly instead of +generating unsuitable values and rejecting them afterward. + +Use Schema to describe basic values and data structures. Then use the `Arbitrary` operations below when you need to +combine or transform those generated values. + +## Looking at Generated Values + +Sampling is useful while choosing your inputs. It is not a test by itself; it simply lets you see whether the generated +values have the shape and size you expected. + +```ts +import { Effect } from "effect" + +const examples = await Effect.runPromise( + Arbitrary.sampleEffect(smallInt, { + count: 10, + size: 5, + seed: "small-integers" + }) +) + +console.log(examples) +``` + +The same generator, seed, size, and Effect version produce the same sequence. A fixed seed is therefore useful in +documentation and while investigating a problem. Ordinary tests usually do not need one: after a failure, Effect +returns a **replay token**, a string that can reproduce both the failing input and the simplification steps that +followed. + +## Combining Generated Values + +In the rest of this guide, “generator” means an `Arbitrary` value. + +Use `Arbitrary.Constant` when a generator should always return an existing value. It is especially useful inside +`flatMap`, where one generated value chooses what should be generated next. A constant does not use randomness and +cannot be simplified further. If it contains an object, every run receives the same object, so the property must not +modify it. + +Use: + +- `map` to transform every generated value; +- `filter` to keep only values that pass a condition; +- `filterMap` to transform a value when the transformation may reject it. + +```ts +import { Result, Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const integers = Arbitrary.schema(Schema.Int) + +const integerOrZero = Arbitrary.schema(Schema.Boolean).pipe( + Arbitrary.flatMap((useFallback) => useFallback ? Arbitrary.Constant(0) : integers) +) + +const nonNegativeLabels = integers.pipe( + Arbitrary.filter((value) => value >= 0), + Arbitrary.map((value) => `integer:${value}`) +) + +const positiveLabels = Arbitrary.filterMap( + integers, + (value) => value > 0 ? Result.succeed(`positive:${value}`) : Result.fail(value) +) +``` + +These operations also apply while Effect simplifies a failing value. A value rejected by `filter` or `filterMap` is not +passed to the property. Effect limits how many generated values may be rejected, so an impossible condition stops with +`SampleError` or `Exhausted` instead of searching forever. + +Prefer a Schema check when it can describe the allowed inputs directly. Effect can often generate matching values +immediately, whereas `filter` must first generate a value and then test it. + +Use `all` to generate independent Arbitraries together. It accepts tuples, other iterables, and records while preserving +their shape: + +```ts +const point = Arbitrary.all([ + Arbitrary.schema(Schema.Number), + Arbitrary.schema(Schema.Number) +]) + +const person = Arbitrary.all({ + name: Arbitrary.schema(Schema.String), + age: Arbitrary.schema(Schema.Int) +}) +``` + +Effect may generate the members in a different internal order, but the returned tuple positions and record keys always +match the input. After a failure, it simplifies one member at a time. Empty tuples and records produce empty values. + +Effect occasionally creates generated records without inherited `Object` methods. This can reveal code that assumes +methods such as `hasOwnProperty` always exist. Prefer `Object.hasOwn(value, key)` when checking generated objects. + +Use `flatMap` when one generated value decides what can be generated next. Create the possible generators before the +callback when you can, because creating a Schema inside the callback repeats that work each time it runs: + +```ts +import { Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const Length = Arbitrary.schema( + Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 4 })) +) + +const StringsByLength = globalThis.Array.from({ length: 4 }, (_, index) => { + const length = index + 1 + return Arbitrary.schema( + Schema.String.check(Schema.isMinLength(length), Schema.isMaxLength(length)) + ) +}) + +const SizedString = Length.pipe( + Arbitrary.flatMap((length) => StringsByLength[length - 1]) +) +``` + +After a failure, Effect first tries simpler values from the first generator and rebuilds the dependent value. It then +tries simpler values from the selected dependent generator. This usually produces a small pair of related values +without breaking the relationship between them. + +Callbacks passed to these operations must return normally, always produce the same result for the same input, finish +in a reasonable time, and avoid modifying generated values. Effect may call them again while simplifying or replaying +a failure. If a callback throws, `sampleEffect` or `checkEffect` reports a **defect**, Effect's term for an unexpected +failure. + +### Targeting Rare Scenarios + +Some behavior can only be tested with a particular combination or sequence of inputs. Making one value appear more +often does not guarantee that combination. For example, generating `Remove` more often still does not guarantee that +it follows an `Insert` for the same key. + +Keep a general property over all command types, then add a focused property whose input already contains the +important transition: + +```ts +import { Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const Key = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 20 })) + +const Insert = Schema.Struct({ + _tag: Schema.Literal("Insert"), + value: Key +}) + +const Remove = Schema.Struct({ + _tag: Schema.Literal("Remove"), + value: Key +}) + +const Contains = Schema.Struct({ + _tag: Schema.Literal("Contains"), + value: Key +}) + +const Command = Schema.Union([Insert, Remove, Contains]) + +const arbitraryHistory = Arbitrary.schema( + Schema.Array(Command).check(Schema.isMaxLength(50)) +) + +const RemoveExistingScenario = Schema.Struct({ + before: Schema.Array(Command).check(Schema.isMaxLength(20)), + key: Key, + after: Schema.Array(Command).check(Schema.isMaxLength(20)) +}) + +const historyContainingRemoveExisting = Arbitrary.schema(RemoveExistingScenario).pipe( + Arbitrary.map(({ before, key, after }) => [ + ...before, + { _tag: "Insert" as const, value: key }, + { _tag: "Remove" as const, value: key }, + ...after + ]) +) +``` + +Use `arbitraryHistory` to explore interactions that were not anticipated. Use `historyContainingRemoveExisting` in a +separate property for behavior that specifically requires removing an existing key. + +This is more reliable than a `frequency` operation, which would only make one choice more likely: + +- every focused run contains the required sequence; +- requirements such as using the same key are visible in the generated input; +- `map` rebuilds the required `Insert` and `Remove` after each simplification, so the final counterexample still tests + the intended behavior; +- the general property remains free to explore all command types. + +If you need to reproduce production traffic proportions, measure throughput, or simulate a random process, use a +workload generator or simulation instead. Those tasks care about exact frequencies; a property test cares about +finding a small input that breaks a rule. + +## Running a Property Directly + +`it.prop` is the shortest way to use a property in a test. Use `Arbitrary.checkEffect` when your program needs to inspect +the result instead of immediately failing a Vitest test. It accepts a function that returns either a boolean or an +`Effect`, and it simplifies the first failing input: + +```ts +import { Effect, Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const values = Arbitrary.schema(Schema.Array(Schema.Int)) + +const program = Arbitrary.checkEffect( + values, + (input) => input.slice().reverse().reverse().every((value, index) => value === input[index]), + { runs: 100, seed: "reverse" } +) + +await Effect.runPromise(program) +// { _tag: "Passed", runs: 100, discards: 0 } +``` + +A property may also return an `Effect`, so it can use Effect services or fail through the Effect error channel: + +```ts +import { Effect, Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const program = Arbitrary.checkEffect( + Arbitrary.schema(Schema.String), + (value) => Effect.succeed(value.length >= 0) +) +``` + +A failed check is returned as a value. Read its `_tag` to see what happened: + +| Result | Meaning | +| ---------------- | -------------------------------------------------------------------------------------- | +| `Passed` | Every requested run passed. | +| `Falsified` | The rule returned `false` or its Effect failed. Includes the simplified failing input. | +| `Exhausted` | Too many generated values were rejected. Includes the seed needed to repeat the run. | +| `ReplayMismatch` | A replay token no longer leads to the same kind of failure. | + +When the returned Effect fails, `Falsified.failure` is a `PropertyError` containing its error value. Returning `false` +produces `ReturnedFalse`. + +Effect keeps those two kinds of failure separate while simplifying an input. A proposed simpler input is not accepted +as the new counterexample if it changes from “returned false” to “Effect failed,” or the other way around. The actual +error value may change; only the kind of failure must stay the same. + +Defects and Effect interruption continue through the Effect returned by `checkEffect` instead of becoming `CheckResult` +values. This allows timeouts and the Effect that started the check to interrupt it normally. + +For the same input and environment, a property must always produce the same result. It must also treat generated values +as read-only. Effect may evaluate a value more than once while simplifying or replaying a failure, and it does not undo +changes made by the property. + +## Turning Requirements into Properties + +Many useful properties are equations. Requirements often contain words such as “same,” “independent of order,” +“reversible,” or “normalizes.” Each word suggests a rule that compares two or more executions of the same code. Such a +rule is often called a **law**. + +### Changing Order or Grouping (Commutativity and Associativity) + +```ts +import { it } from "@effect/vitest" +import { Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const smallInt = Arbitrary.schema( + Schema.Int.check(Schema.isBetween({ minimum: -100, maximum: 100 })) +) + +it.prop( + "addition is commutative", + { a: smallInt, b: smallInt }, + ({ a, b }) => a + b === b + a +) + +it.prop( + "addition is associative", + { a: smallInt, b: smallInt, c: smallInt }, + ({ a, b, c }) => (a + b) + c === a + (b + c) +) +``` + +The bounds are part of the meaning of these tests. They keep the generated values inside the region where JavaScript +integer arithmetic behaves like the algebra we intend to test. + +### Running an Operation Twice Changes Nothing (Idempotence) + +Normalization is commonly idempotent: once a value is normalized, applying the operation again should do nothing. + +```ts +const clampNonNegative = (value: number): number => Math.max(0, value) + +it.prop( + "clamping is idempotent", + [smallInt], + ([value]) => clampNonNegative(clampNonNegative(value)) === clampNonNegative(value) +) +``` + +### Applying an Operation Twice Returns the Original (Involution) + +An involution returns to the original value when applied twice. Negation is the smallest example: + +```ts +it.prop( + "negation is an involution", + [smallInt], + ([value]) => -(-value) === value +) +``` + +### Comparing Two Implementations + +When replacing or optimizing an implementation, compare the two functions over the same generated inputs: + +```ts +const doubleByAddition = (value: number): number => value + value +const doubleByMultiplication = (value: number): number => value * 2 + +it.prop( + "the two double implementations agree", + [smallInt], + ([value]) => doubleByAddition(value) === doubleByMultiplication(value) +) +``` + +### Operations That Undo Each Other + +```ts +const increment = (value: number): number => value + 1 +const decrement = (value: number): number => value - 1 + +it.prop( + "increment and decrement are inverses", + [smallInt], + ([value]) => decrement(increment(value)) === value +) +``` + +The `Arbitrary` module does not provide a separate helper for each kind of law. These comparisons are short to write +directly in TypeScript. A project that applies the same laws to many data types can still define its own reusable test +functions. + +## Required Inputs and Common Traps + +Each property should be able to fail, be easy to explain, and come from a real requirement. Adding more conditions +without clarifying the allowed inputs often makes a test harder to understand rather than more useful. + +### Operations That Reject Some Inputs + +If an operation only accepts some inputs, describe that restriction in the Schema when possible. For division, a union +can generate non-zero integers directly: + +```ts +const NonZeroSmallInt = Schema.Union([ + Schema.Int.check(Schema.isBetween({ minimum: -100, maximum: -1 })), + Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 })) +]) + +const nonZeroSmallInt = Arbitrary.schema(NonZeroSmallInt) + +it.prop("a non-zero integer divided by itself is one", [nonZeroSmallInt], ([value]) => value / value === 1) +``` + +Use `Arbitrary.filter` when Schema cannot express the condition: + +```ts +const odd = smallInt.pipe( + Arbitrary.filter((value) => value % 2 !== 0) +) +``` + +Rejected generated values count against `maxDiscards`. If very few values pass the filter, the check may return +`Exhausted` before running the property enough times. Prefer a Schema check or `flatMap` when either can generate valid +values directly. + +### Floating-Point Laws + +Floating-point arithmetic needs a different rule. Use `Schema.Finite` when `NaN` and infinities are outside the allowed +inputs. When exact equality is not the real requirement, compare results with an acceptable error tolerance. + +### Mutable Properties + +Effect does not clone generated inputs before evaluating them. A property that changes its input may change the +reported counterexample and make simplification or replay unreliable. Treat generated values as read-only, and create +mutable test data inside each property evaluation rather than sharing it between runs. + +## Laws Can Still Miss Visible Bugs + +Passing a familiar list of laws does not prove that every public operation is correct. Two values may represent the +same logical result while an operation accidentally depends on how they are stored internally. + +Consider an immutable first-in-first-out queue represented by a front array and a reversed rear array. The balancing +and update operations below are correct, but `front` deliberately reads the last front element instead of the first: + +```ts +import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +interface Queue { + readonly front: ReadonlyArray + readonly rear: ReadonlyArray +} + +const balance = (front: ReadonlyArray, rear: ReadonlyArray): Queue => + front.length === 0 ? { front: [...rear].reverse(), rear: [] } : { front, rear } + +const empty = (): Queue => balance([], []) +const isEmpty = (queue: Queue): boolean => queue.front.length === 0 + +const enqueue = (value: number, queue: Queue): Queue => balance(queue.front, [value, ...queue.rear]) + +const dequeue = (queue: Queue): Queue => balance(queue.front.slice(1), queue.rear) + +// Deliberately wrong: a FIFO queue should return queue.front[0]. +const front = (queue: Queue): number => queue.front[queue.front.length - 1]! + +const toArray = (queue: Queue): ReadonlyArray => [ + ...queue.front, + ...[...queue.rear].reverse() +] + +const equals = (left: Queue, right: Queue): boolean => { + const a = toArray(left) + const b = toArray(right) + return a.length === b.length && a.every((value, index) => value === b[index]) +} + +const Item = Schema.Int.check( + Schema.isBetween({ minimum: -100, maximum: 100 }) +) +const Items = Schema.Array(Item).check(Schema.isMaxLength(8)) +const NonEmptyItems = Items.check(Schema.isMinLength(1)) + +const item = Arbitrary.schema(Item) +const items = Arbitrary.schema(Items) +const nonEmptyItems = Arbitrary.schema(NonEmptyItems) + +const queue = Arbitrary.all({ + front: items, + rear: items +}).pipe( + Arbitrary.map(({ front, rear }) => balance(front, rear)) +) + +const nonEmptyQueue = Arbitrary.all({ + front: nonEmptyItems, + rear: items +}).pipe( + Arbitrary.map(({ front, rear }) => balance(front, rear)) +) +``` + +The queue is intended to satisfy these equations: + +1. `isEmpty(empty()) === true` +2. `isEmpty(enqueue(x, q)) === false` +3. `front(enqueue(x, empty())) === x` +4. For non-empty `q`, `front(enqueue(x, q)) === front(q)` +5. `dequeue(enqueue(x, empty()))` equals `empty()` +6. For non-empty `q`, `dequeue(enqueue(x, q))` equals `enqueue(x, dequeue(q))` + +All six pass, even with the broken `front` operation: + +```ts +describe("queue laws", () => { + it("Q1", () => assert.isTrue(isEmpty(empty()))) + + it.prop("Q2", [item, queue], ([x, q]) => !isEmpty(enqueue(x, q))) + + it.prop("Q3", [item], ([x]) => front(enqueue(x, empty())) === x) + + it.prop( + "Q4", + [item, nonEmptyQueue], + ([x, q]) => front(enqueue(x, q)) === front(q) + ) + + it.prop( + "Q5", + [item], + ([x]) => equals(dequeue(enqueue(x, empty())), empty()) + ) + + it.prop( + "Q6", + [item, nonEmptyQueue], + ([x, q]) => equals(dequeue(enqueue(x, q)), enqueue(x, dequeue(q))) + ) +}) +``` + +The equations compare queues through `equals`, which checks their logical sequence. But replacing a queue with an equal +queue must not change the answer returned by `front`. + +We can test that hidden requirement by substituting both sides of Q6 into `front`: + +```ts +it.prop( + "front agrees after the Q6 rewrite", + [item, nonEmptyQueue], + ([x, q]) => { + const left = dequeue(enqueue(x, q)) + const right = enqueue(x, dequeue(q)) + return front(left) === front(right) + }, + { + // This flag belongs only in the tutorial while `front` is intentionally broken. + fails: true, + arbitrary: { + runs: 1_000, + size: 10, + seed: "front-after-rewrite" + } + } +) +``` + +This property fails and simplifies to a small queue. The first six laws say that two queue expressions are equal. The +new property also checks that `front` gives the same answer for both expressions. + +The general lesson is simple: whenever a law says `left` and `right` are equal, try both values as inputs to public +operations. Their results should remain equal. This does not prove every possible combination, but it can reveal code +that accidentally reads an internal representation. + +## Comparing with a Simple Model + +Some systems are easier to check against a small, straightforward implementation than against a list of equations. +This simpler implementation is called a **model**. It may be slower than the real code; its advantage is that it is +easy to understand and trust. + +The following property checks Effect's immutable `HashSet` against JavaScript's mutable `Set`. Instead of generating +sets directly, it generates command sequences and runs the same history against both implementations. + +```ts +import { it } from "@effect/vitest" +import { HashSet, Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const Key = Schema.Int.check( + Schema.isBetween({ minimum: 0, maximum: 20 }) +) + +const Command = Schema.Union([ + Schema.Struct({ _tag: Schema.Literal("Insert"), value: Key }), + Schema.Struct({ _tag: Schema.Literal("Remove"), value: Key }), + Schema.Struct({ _tag: Schema.Literal("Contains"), value: Key }) +]) + +const commands = Arbitrary.schema( + Schema.Array(Command).check(Schema.isMaxLength(50)) +) + +it.prop("HashSet agrees with the Set model", [commands], ([input]) => { + const model = new Set() + let actual = HashSet.empty() + const modelTrace: Array = [] + const actualTrace: Array = [] + + for (const command of input) { + switch (command._tag) { + case "Insert": + model.add(command.value) + actual = HashSet.add(actual, command.value) + break + + case "Remove": + model.delete(command.value) + actual = HashSet.remove(actual, command.value) + break + + case "Contains": + modelTrace.push(model.has(command.value)) + actualTrace.push(HashSet.has(actual, command.value)) + break + } + } + + return modelTrace.length === actualTrace.length && + modelTrace.every((value, index) => value === actualTrace[index]) && + model.size === HashSet.size(actual) && + [...model].every((value) => HashSet.has(actual, value)) +}) +``` + +The property compares two things: + +- the recorded answers verify each `Contains` operation; +- the final membership and size checks verify that both states ended with the same contents. + +When this test fails, Effect simplifies the command list and its values. The final counterexample is often a short +history that clearly shows which state change the implementation handled incorrectly. + +## Replaying a Failure + +Every `Falsified` result contains a `replay` token. It is a string produced by Effect and is not meant to be edited. The +token records enough information to generate the same initial input and repeat the simplification steps that led to the +reported counterexample: + +```ts +import { Effect, Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const arbitrary = Arbitrary.schema(Schema.Int) + +const program = Effect.gen(function*() { + const first = yield* Arbitrary.checkEffect(arbitrary, (value) => value < 10, { + seed: "integer-bound" + }) + + if (first._tag === "Falsified") { + const replayed = yield* Arbitrary.checkEffect( + arbitrary, + (value) => value < 10, + { replay: first.replay } + ) + return replayed + } + + return first +}) +``` + +Store the token in logs or failure output when you want to reproduce a failure locally. Because this module is +unstable, a token created by one Effect release may not work with another. For a permanent regression test, copy the +final failing input into an ordinary example-based test. + +When `replay` is present, its recorded settings replace `runs`, `size`, `maxDiscards`, `maxShrinks`, and `seed`. + +Effect returns `ReplayMismatch` when the Schema, property, or generator has changed enough that the token no longer +leads to the same kind of failure. The token does not store the exact final input or exact Effect error, so replay may +still succeed if the error value changes while the property continues to fail in the same way. + +## Sampling Options + +`Arbitrary.sampleEffect` accepts the following options: + +| Option | Default | Meaning | +| ------------- | ---------------------------- | ---------------------------------------------------------- | +| `count` | `10` | Number of values to return. | +| `size` | `10` | Rough complexity of each generated value. | +| `maxDiscards` | `max(100, count * 10)` | Maximum rejected values before failing with `SampleError`. | +| `seed` | A value from Effect `Random` | String or number used to reproduce the generated sequence. | + +If too many values are rejected, the Effect fails with a `SampleError`. The error contains the number of accepted and +rejected values and the seed used for the run. Passing that seed to another `sampleEffect` call repeats the run even if +the first call did not specify a seed. + +## Check Options + +`Arbitrary.checkEffect` accepts the following options: + +| Option | Default | Meaning | +| ------------- | ---------------------------- | ---------------------------------------------------------------------- | +| `runs` | `100` | Number of successful generations and property evaluations to complete. | +| `size` | `10` | Largest approximate input complexity. It grows during the check. | +| `maxDiscards` | `max(100, runs * 10)` | Maximum rejected values before returning `Exhausted`. | +| `maxShrinks` | `100` | Maximum simplifications tried after the first failure. | +| `seed` | A value from Effect `Random` | String or number used to reproduce generation. | +| `replay` | None | Token from a previous `Falsified` result. | + +Rejected values do not count toward `runs`, and the input size does not grow after a rejection. When `runs` is `1`, +Effect uses the configured `size` immediately. + +After a failure, `maxShrinks` counts every simpler input that Effect examines, including values rejected by a filter and +values that fail in a different way. The `shrinks` field in a `Falsified` result counts only the simplifications that +became the new best counterexample. + +`size` is a guide rather than a maximum length for the whole result. Separate strings, collections, and object fields +may each use it. Recursive structures share it so that the complete value remains finite. Explicit Schema minimum and +maximum checks still take priority. + +## How Effect Builds a Generator from Schema + +This section is useful when a Schema contains custom checks, recursion, or declarations. If you only use ordinary +Schemas, you can skip to [Using `@effect/vitest`](#using-effectvitest). + +`Arbitrary.schema` generates the value represented by a Schema after decoding. For example, +`Schema.NumberFromString` represents a number encoded as a string, so its generator produces numbers rather than +strings. + +Effect prepares the generator when `Arbitrary.schema` is called. If it cannot support the Schema, the call throws +immediately rather than returning a generator that fails later. + +### Schema Checks and Rejected Values + +Effect can use many common Schema checks while generating values, including: + +- minimum and maximum values, including values that use a custom `Order`; +- finite and integer numbers; +- minimum and maximum lengths for strings, collections, and object properties; +- supported regular-expression patterns; +- checks that collection values are unique. + +For these checks, Effect produces matching values directly. It applies other custom checks after generation. A value +that fails one of those checks is rejected. + +`maxDiscards` limits how many values may be rejected. An impossible or very selective check therefore produces +`SampleError` or `Exhausted` instead of searching forever. + +### Custom Shrinking + +The default simplification is usually enough: Effect removes collection items, moves numbers and strings toward +simpler values, and simplifies object or tuple fields one at a time while preserving Schema checks. Use the `shrink` +option when your application has a useful simplification that Effect cannot infer from the data structure. + +For example, an addition expression can be replaced directly by either side. The default behavior cannot infer that +meaning from the object shape: + +```ts +import { Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +interface Literal { + readonly _tag: "Literal" + readonly value: number +} + +interface Add { + readonly _tag: "Add" + readonly left: Expression + readonly right: Expression +} + +type Expression = Literal | Add + +const Expression: Schema.Codec = Schema.suspend(() => + Schema.Union([ + Schema.Struct({ + _tag: Schema.Literal("Literal"), + value: Schema.Int + }), + Schema.Struct({ + _tag: Schema.Literal("Add"), + left: Expression, + right: Expression + }) + ]) +) + +const shrinkExpression = (expression: Expression): ReadonlyArray => { + switch (expression._tag) { + case "Literal": + return expression.value === 0 ? [] : [{ _tag: "Literal", value: 0 }] + case "Add": + return [ + expression.left, + expression.right, + ...shrinkExpression(expression.left).map((left) => ({ ...expression, left })), + ...shrinkExpression(expression.right).map((right) => ({ ...expression, right })) + ] + } +} + +const expressions = Arbitrary.schema(Expression, { + shrink: shrinkExpression +}) +``` + +Initial values still come from `Expression`. After a property fails, `shrinkExpression` tells Effect which simpler +values to try next. Replacing `Add(left, right)` with `left` or `right` can reach a useful counterexample much faster +than changing one field at a time. + +Before the property sees a proposed value, Effect checks it with `Expression`. Invalid values are skipped and consume +one unit of `maxShrinks`; Effect does not ask the callback to simplify them further. The Schema therefore remains the +final authority even if the callback contains a cast or calls untyped code. + +Effect calls the callback only after a property fails. It may call it again during simplification or replay. The +callback must therefore finish normally, always return the same proposed values for the same input, and avoid modifying +data. It should list the most useful simplifications first. Returning `[]` means that the current value cannot be +simplified. + +A custom `shrink` callback replaces the default behavior. Keep the default when simplifying fields and collection +items is enough. Prefer `map` or `toCodecArbitrary` when you can generate a simpler representation and transform it. +Use `shrink` only for application-specific shortcuts that those approaches cannot express clearly. + +### Recursive Schemas + +Recursive Schemas are supported as long as there is a way for generation to stop: + +```ts +import { Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +interface Node { + readonly value: string + readonly children: ReadonlyArray +} + +const Node: Schema.Codec = Schema.Struct({ + value: Schema.String, + children: Schema.Array(Schema.suspend(() => Node)).check(Schema.isMaxLength(3)) +}) + +const nodes = Arbitrary.schema(Node) +``` + +Here, an empty `children` array stops the recursion. Effect limits recursion across the whole generated value so that +separate branches cannot each grow without regard for the others. + +Effect ignores a recursive alternative that can never stop. If the complete Schema has no way to produce a finite +value, `Arbitrary.schema` throws immediately. You do not need to provide a special terminal generator or depth marker. + +### Declaration Schemas + +`Schema.declare` can describe a type whose internal structure is hidden from the generic Schema machinery. To generate +such a value, Effect looks for a simpler Schema representation in this order: + +1. an explicit `toCodecArbitrary`; +2. a representation provided by Effect for one of its built-in types; +3. `toCodecJson`; +4. `toCodec`. + +Most declarations already provide a usable conversion and need no Arbitrary-specific setup. Add `toCodecArbitrary` +only when the usual representation cannot be generated or produces poor test inputs. + +If `toCodecJson` is present but returns `undefined`, the declaration explicitly says that its JSON representation is +hidden. Generation stops with an error instead of trying `toCodec`. + +`toCodecArbitrary` returns a Schema `Link`, not an `Arbitrary`. The first Schema in the link describes values that are +easy to generate, and the transformation converts them into the declared type: + +```ts +import { Schema, SchemaTransformation } from "effect" + +class UserId { + readonly value: number + constructor(value: number) { + this.value = value + } +} + +const UserIdSchema = Schema.instanceOf(UserId, { + toCodecArbitrary: () => + Schema.link()( + Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000_000 })), + SchemaTransformation.transform({ + decode: (value) => new UserId(value), + encode: (id) => id.value + }) + ) +}) +``` + +Effect checks converted values against the original declaration. The conversion may reject some values; those values +count toward `maxDiscards`, and Effect can continue with others. Normal decode errors reject a value, while unexpected +defects and interruption fail the sampling or checking Effect. + +The callback also receives: + +- the value types represented by declarations with type parameters; +- the common Schema checks that Effect recognized for the declaration. + +Built-in collection types use arrays as their simpler generation representation. Map keys and set values remain +unique. Effect collections such as `HashMap`, `HashSet`, and `Chunk`, and types such as `Graph`, `BigDecimal`, and date +and time values, provide their own links when that produces better inputs. A declaration whose usual conversion already +works needs no Arbitrary-specific annotation. + +## Using `@effect/vitest` + +`@effect/vitest` accepts arrays or records containing Schemas, Arbitraries, or both: + +```ts +import { assert, it } from "@effect/vitest" +import { Effect, Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const Name = Arbitrary.schema(Schema.Literals(["Ada", "Grace"])) + +it.prop( + "integer addition is commutative", + [Schema.Int, Schema.Int], + ([a, b]) => a + b === b + a, + { arbitrary: { runs: 200, seed: "addition" } } +) + +it.effect.prop( + "generated values can be checked in an Effect", + { name: Name, value: Schema.Int }, + ({ name, value }) => + Effect.sync(() => { + assert.include(["Ada", "Grace"], name) + assert.isTrue(Number.isInteger(value)) + }), + { arbitrary: { runs: 50 } } +) +``` + +Generators from other property-testing libraries are not supported. Use Effect Arbitraries when an input needs more +composition than a Schema alone can provide. + +`@effect/vitest` turns `Falsified`, `Exhausted`, and `ReplayMismatch` results into test failures. The failure message +includes the simplified failing input and replay token. Diagnostic values use `Formatter.format`, so strings remain quoted and +escaped, collections such as `Map` and `Set` retain their structure, and application values can provide a custom +`toString`. + +The Vitest integration reports a property failure when the property returns `false`, throws, or returns a failed Effect +for a reason other than interruption. It then simplifies and reports the failing input. Effect interruption remains an +interruption. Any normal return value other than `false`, including `void`, passes for that input. + +The Vitest `timeout` interrupts the Effect fiber that generates inputs, runs the property, and simplifies failures. +Effect finalizers still run. A timeout cannot stop a synchronous JavaScript callback until that callback returns. + +## Current Limitations + +The module starts from Schema and currently does not provide: + +- separate Arbitrary constructors for strings, numbers, arrays, and objects; describe those inputs with Schema; +- a public low-level generator constructor or direct access to simplification steps; +- weighted choice; use [targeted scenarios](#targeting-rare-scenarios) to guarantee that a test reaches an important + situation; +- support for generators from other property-testing libraries; +- running generated inputs in parallel; +- automatic test failure messages outside `@effect/vitest`; +- replay tokens guaranteed to work across releases of this unstable module. + +These limitations may change while the module remains unstable. + +## Summary + +Property-based testing is most useful when the property says more than “this function did not throw.” Start by choosing +the allowed inputs and a rule that comes from the requirement: + +- use Schema checks to generate valid values; +- combine independent inputs with records, tuples, or `Arbitrary.all`; +- use `it.prop` and `it.effect.prop` in tests, and `checkEffect` when result values or replay are part of the + program; +- write laws directly and remember the limits of JavaScript numbers; +- when two values are considered equal, check that public operations treat them the same way; +- use command sequences and a simple model for stateful behavior; +- pay attention to rejected values, simplification, and replay because they affect what the test actually checks. + +The most important choice is not how many random inputs to generate. It is choosing inputs and a rule that make a small +counterexample useful. + +## Appendix: Design and Implementation Decisions + +This appendix is for people who maintain or extend the module. You can use `Arbitrary` without reading it. Internal type +names appear only where they help connect a decision to the code, and each one is explained when it first appears. + +### Why Effect Has Its Own Implementation + +Effect owns the property-testing implementation so it can provide: + +- **Automatic support for recursion.** Effect finds the paths that let recursive Schemas stop. If no such path exists, + `Arbitrary.schema` fails immediately. All recursive branches in one value share the same limit. +- **A limit on rejected values.** `maxDiscards` stops generation when a condition is impossible or accepts too few + values. `maxShrinks` also limits rejected values examined while simplifying a failure. +- **Normal Effect behavior.** Sampling and checking can be interrupted, use services, and fail through Effect. Direct + checks, `TestSchema`, and `@effect/vitest` use the same implementation. +- **Direct composition.** `all` combines existing Arbitraries without creating a temporary Schema. +- **One replay token.** The token records the initial attempt and every accepted simplification. Replay regenerates the + initial value, runs the property again, and follows those simplifications. +- **Useful behavior for `flatMap`.** When the first generated value becomes simpler, Effect rebuilds the value that + depends on it while keeping later random choices stable. Nested `flatMap` calls share the same recursion limit. +- **Small common paths.** Common Schema shapes use direct loops, and sampling does not build simplification data. Bundle + and runtime benchmarks watch these paths, but do not promise an exact performance level. + +### Interface Design Decisions + +These decisions explain why the public API contains some operations and omits others. Each section also says what new +use case would justify changing the decision. + +#### Use Schema as the Public Generation Language + +**Status:** Use Schema as the single public language for describing generated data. + +Schema already describes primitive values, data structures, checks, conversions, declarations, and recursion. +Duplicating those features with `Arbitrary.String`, `Arbitrary.Array`, and similar constructors would give users two +different ways to describe the same inputs. + +The module therefore exposes `schema` and a small set of operations for combining existing Arbitraries. A declaration +whose structure is hidden can provide a simpler Schema through `toCodecArbitrary`; it does not need a second language +for generators. + +The implementation stored inside `Arbitrary` remains private. Users can combine and run an Arbitrary without +depending on how Effect currently generates or simplifies values. Reconsider this decision only if an important input +cannot be described by Schema or built clearly from existing Arbitraries. + +#### Keep Low-Level Arbitrary Construction Internal + +**Status:** Do not expose `Arbitrary.make` for the current internal generator type. + +Applications can already build inputs through: + +- `Arbitrary.schema` for primitive values, data structures, checked values, and recursive values; +- `Constant` for lifting an already constructed value into dependent generation; +- `map`, `filter`, `filterMap`, `flatMap`, and `all` for composition; +- `toCodecArbitrary` when a Schema declaration hides its structure and needs a simpler Schema for generation. + +The internal `Generator` does more than turn random numbers into values. It also tracks the minimum space needed for +recursion, rejected values, later simplifications, interruption, and replay positions. Exposing it would require every +custom generator to understand those rules. + +A simpler public constructor that ignores some rules would create Arbitraries that behave differently when combined or +simplified. Reconsider this decision when at least two real generators cannot be expressed through Schema and the +existing operations. Any proposal should keep the internal bookkeeping out of application code. + +#### Provide a Constant Constructor + +**Status:** Provide `Arbitrary.Constant` for an existing value. + +Without `Constant`, callers must generate an unrelated value and map it to the desired constant. `Schema.Literal` +supports only literal values and is awkward to rebuild repeatedly inside `flatMap`. + +`Constant` uses no randomness and offers no simpler values of its own. The value that selected it through `flatMap` can +still be simplified. Every run returns the same value, so objects are not cloned and properties must not modify them. + +#### Use Schema Union for Static Choice + +**Status:** Do not add a second operation for choosing uniformly among fixed alternatives. + +`Arbitrary.schema(Schema.Union([...]))` already chooses among alternatives. Because the alternatives remain Schemas, +Effect can see their checks, recursion needs, and validation rules. `Arbitrary.oneOf` would duplicate that behavior +without supporting a new use case. + +A weighted choice would add probabilities, but probabilities do not guarantee that a property reaches an important +situation. The later decision about weighted choice explains the preferred alternative. + +#### Keep Size Management Internal + +**Status:** Do not pass the current `size` to an `Arbitrary.sized` callback. + +The runner's `size` option already lets strings, collections, and recursive Schemas become more complex as a check +progresses. Most users therefore do not need to write their own size rules. + +Passing this value to callbacks would make the runner's growth policy part of the public composition API. It is mainly +useful for low-level custom generators, which the module does not currently expose. Reconsider this decision when an +important input needs a size rule that Schema checks and recursion cannot describe. + +#### Keep Custom Shrinking at the Schema Boundary + +**Status:** Add custom simplification only through `Arbitrary.schema(schema, { shrink })`. + +The Schema produces every initial value and checks every value proposed by the callback. Invalid proposals are skipped; +valid proposals can be simplified again with the same callback. Effect calls the callback only when needed and records +its stable order for replay. + +The callback replaces the default simplification order. Mixing two independent orders would make `maxShrinks` and +replay positions hard to predict. + +A general `Arbitrary.reshrink` could not provide the same automatic validation. An Arbitrary built with `Constant`, +`map`, `flatMap`, or `all` does not retain one original Schema. Reconsider a general operation only when a real input +cannot be represented by Schema and needs its own simplification. A proposal must explain how values are checked, how +the two simplification strategies interact, and how replay remains stable. + +#### Prefer Targeted Scenarios to Weighted Choice + +**Status:** Do not add `Arbitrary.frequency` merely to reach important test cases more often. + +Weights only change how often a branch is chosen. They cannot guarantee the combination of values that makes a behavior +important. A general property over `Schema.Union`, plus a focused property whose input already contains the important +situation, states the requirement directly and preserves it during simplification. + +Workloads that must match real probabilities belong in simulations or benchmarks. Reconsider weighted generation only +if a correctness rule itself depends on a distribution, rather than merely needing better test coverage. + +#### Name Effectful Runners Explicitly + +**Status:** Keep the `Effect` suffix on `sampleEffect` and `checkEffect`. + +The names show that these functions return an Effect. They also leave clear names available if a real need for +synchronous versions appears later; no such version is currently promised. + +#### Current Interface Scope + +The module focuses on inputs described by Schema and checks executed with Effect. It guarantees that generated values +match their Schema, rejection is limited, recursion stops, and replay works within the same implementation. Exact +generated sequences, probabilities, and intermediate simplifications may change. + +The unstable interface does not currently expose: + +- separate Arbitrary constructors for primitive values and structures, or direct access to internal samples and + simplification steps; +- weighted distribution controls; +- custom simplification that is not attached to a Schema; +- support for generators from other libraries or a broad set of runner settings; +- parallel property evaluation; +- test-runner assertion integration outside `@effect/vitest`; +- replay tokens guaranteed to work across releases. + +These omissions are not necessarily permanent. They let Schema remain responsible for describing valid values and let +Effect change the internal generator while each proposed API addition is considered separately. + +### Technical Decisions + +The remaining sections describe the current implementation. These details matter to maintainers because changing them +can alter generated values, simplification, replay, performance, or bundle size without changing the public types. + +#### Representing Generation + +- The internal `Generator` stores `minCost`, the minimum space needed to produce a value, and `generate`, the + generation function. A Schema being prepared uses `Compiled`, which also records its dependencies. Arbitrary + operations outside Schema do not join that dependency graph. +- One call returns `Generated` when it produced a value or `Discarded` when it rejected the attempt. It returns directly + when possible and uses an Effect only when needed. Internal mapping uses Effect's eager operations so an immediate + result remains synchronous. +- A generated value may provide a `Pull`: an internal operation that returns one proposed simplification at a time. + Effect creates this sequence only when needed. Sampling does not create it, and rejected proposals remain visible to + the runner so they count toward `maxShrinks`. Operations such as `filter` can skip a rejected proposal and continue + with simpler values that follow from it. + +#### Preparing a Schema + +- Preparation starts with `SchemaAST.toType`, which selects the value after decoding rather than its encoded form. +- The implementation reads Schema's existing internal syntax tree, called its AST, instead of creating a second tree + for Arbitrary. This preserves the original order of checks, declaration links, type parameters, recursive references, + and error paths. +- Preparation happens immediately and reuses work for repeated nodes in the same Schema. Unsupported declarations, + contradictory bounds, and recursion with no stopping point fail during `Arbitrary.schema`. Combinations that are + valid but impossible to satisfy eventually return `SampleError` or `Exhausted`. +- Effect builds common checks into generation and still runs the original checks afterward. Ordinary Schema nodes do + not use the complete Schema parser for every value. Declaration links are converted and then checked against the + original declaration. +- For supported regular expressions, Effect generates matching text directly and checks the pattern afterward. It + chooses uniformly among supported patterns. Unsupported patterns act as ordinary filters instead of making the + complete Schema unsupported. +- Prepared patterns cache valid UTF-16 lengths and reusable character information. The same information helps simplify + generated strings without changing which strings the pattern accepts. + +#### Declarations + +- Effect looks for a declaration representation in this order: `toCodecArbitrary`, a built-in representation, + `toCodecJson`, then `toCodec`. If `toCodecJson()` returns `undefined`, the declaration says its JSON form is hidden, + so preparation stops instead of silently trying another conversion. +- `toCodecArbitrary` returns a Schema `Link`. Effect generates the source Schema, converts it, and checks the result + against the original declaration. Rejected initial values count toward `maxDiscards`; rejected simplifications count + toward `maxShrinks` while later valid proposals remain available. +- The callback receives the represented type parameters and the common checks Effect recognized. `ReadonlyMap` and + `ReadonlySet` use arrays internally. `HashMap`, `HashSet`, `Chunk`, and other specialized types keep local links when + that avoids loading their implementation into every use of the generic Schema compiler. +- Effect may use an `Order` while combining bounds, but passes only the final bounds to `toCodecArbitrary`. The link is + responsible for producing suitable values, and the original declaration rejects incompatible results. +- Links used only for generation cannot encode values; generation calls only their decode direction. + +#### Recursion and Size + +- Effect examines recursive references as a graph and computes the minimum space needed to reach a non-recursive value. + A recursive alternative that can never stop is ignored; a complete Schema that can never stop is rejected + immediately. +- Each attempt receives the minimum required space plus the configured `size`. Crossing a recursive reference consumes + some of that allowance. Effect reserves enough for required child values before generating optional siblings. + Recursive siblings are tried in a changing order so one declaration position does not always receive more space, but + the returned object keeps its declared order. +- Every recursive branch in the generated value shares one allowance. Nested composition does not create fresh space. +- Strings and collections also use `size` as a rough complexity target. Sampling uses a fixed value; checking raises it + after successful runs. Schema minimums are always respected, maximums remain limits, and rejected attempts do not + increase the size. + +#### Randomness and Probabilities + +- The runner starts from one seed and creates a separate random-number state for each attempt. Replay can therefore jump + directly to a recorded attempt, and a property's use of Effect `Random` cannot change later generated inputs. +- On some predictable attempts, generated records have no prototype. Choosing this case does not consume a random + number, and every record produced during that attempt uses the same choice, including simplified values. This applies + to `Schema.Struct`, `Schema.Record`, `Schema.Json`, and record-shaped `all`, but not to arrays, tuples, declarations, or + collection classes. +- Integer and BigInt generation avoids favoring some values accidentally. It tries boundary values more often on some + runs. Number generation includes signed zero, very small values, infinities, and `NaN` when the Schema permits them. + Finite and integer checks exclude the values they promise to exclude. +- The magnitude of unbounded integers grows with `size`. Ordinary strings combine printable ASCII with a fixed set of + JavaScript edge cases. Regular-expression length is measured in UTF-16 code units, matching JavaScript strings. +- Exact probabilities, the value produced by a particular seed, and the order of simplifications may change. Source + code comments credit algorithms adapted from other property-testing and random-number implementations. + +#### Generating and Simplifying Data Structures + +- Arrays first remove optional or repeated items and then simplify remaining items. Objects choose optional properties + without favoring earlier declarations. If the first choice is too large for the available recursion space, Effect + uses the smallest choice that still satisfies the Schema instead of rejecting a Schema that can produce a value. + Simplification removes optional properties, simplifies values, and then simplifies generated keys while keeping keys + unique. +- Generation of unique collections has a retry limit. `Schema.isUnique()` compares complete values; + `Schema.isUniqueKey()` compares the keys of Map entries. Both use Effect equality. Primitive values use specialized + tracking, while objects use Effect `Hash` and `Equal`. +- A Schema union chooses uniformly among alternatives that fit in the remaining recursion space. During simplification, + it first tries the earliest alternative with the lowest required space when that alternative is cheaper, then + simplifies the selected alternative. Separate random-number state keeps unrelated later values from changing this + fallback. For `oneOf`, a value must match exactly one alternative; overlaps are rejected. +- `all` adds the minimum space required by its members and makes them share one recursion allowance. It changes their + internal generation order for fairness, then restores tuple positions and record keys. Simplification changes one + member at a time. +- `map` transforms every proposed value without consuming randomness or changing replay positions. `filter` and + `filterMap` reject values that do not pass their condition. During simplification, Effect skips rejected proposals and + can continue to later valid ones. `map` keeps duplicate transformed values because removing them would change replay + positions. Each rejection consumes one unit of `maxShrinks` without running the property. +- `filterMap` ignores the failure value in `Result`. Separate implementations for `map` and `filter` avoid allocating a + `Result` in these common cases. +- `Arbitrary.schema(schema, { shrink })` keeps normal generation but replaces default simplification. Effect calls the + callback only when needed and checks each proposal against the Schema value after decoding. Invalid proposals consume + `maxShrinks`, never reach the property, and are not simplified further. + +#### Dependent Generation with `flatMap` + +- Effect first generates the source value, calls the callback, and then runs the Arbitrary selected by that callback. + The selected Arbitrary always receives enough space to produce its smallest possible value. +- Simplification starts with the source. Each simpler source value selects a new dependent Arbitrary. Effect later tries + simpler values from the current dependent Arbitrary. Once it accepts one of those dependent values, it no longer + returns to source simplification on that path. +- If the initial source or dependent Arbitrary rejects its value, the complete attempt is rejected. If a dependent value + selected during simplification is rejected, it consumes one unit of `maxShrinks`, and Effect continues with later + simplifications of the source. +- Sampling needs no saved random state because it does not simplify values. Checking saves the random state immediately + after source generation. The initial dependent value and every dependent value chosen from a simpler source receive a + separate copy, so trying one simplification cannot change another or affect later generated inputs. +- The dependent Arbitrary temporarily receives the minimum extra recursion space it needs. Any unused extra space is + removed afterward, so nested `flatMap` calls still share one overall allowance. +- Calling `Arbitrary.schema` inside the callback prepares that Schema on every callback call. Effect does not cache it + automatically. + +#### Running, Replaying, and Interrupting Checks + +- `sampleEffect` fails with `SampleError`. `checkEffect` returns `Passed`, `Falsified`, `Exhausted`, or + `ReplayMismatch`. `SampleError` and `Exhausted` include the seed so the run can be repeated. Only the boolean `true` + passes. Returning `false` and returning a failed Effect are different kinds of property failure, and simplification + preserves the original kind. The exact Effect error may change. Unexpected defects and interruption remain Effect + failures rather than result values. +- After a failure, Effect follows the first proposed simpler value that fails in the same way. `maxShrinks` counts every + proposal examined, including rejected values; `shrinks` counts only proposals accepted as the new counterexample. + Property runs do not include these extra evaluations. When the limit is reached, Effect returns the most simplified + failing input found so far. It does not clone or freeze generated values. +- A replay token records the seed, attempt number, size, original kind of failure, and the position of every accepted + simplification. Replay rebuilds those values instead of storing them in the token. It returns `ReplayMismatch` when a + position no longer exists or no longer fails in the same way. It does not compare the exact final input or Effect + error. Malformed tokens may fail with a defect, and tokens need not work across releases while the module is unstable. + Replay follows the recorded positions directly, so it ignores `maxShrinks`. +- Long synchronous generation loops occasionally yield control according to `Scheduler.MaxOpsBeforeYield`. Generation + that uses Effects, declaration conversion, property evaluation, and simplification can all be interrupted normally. diff --git a/repos/effect/packages/effect/CONFIG.md b/repos/effect/packages/effect/CONFIG.md index 8b932b7901..bcb4145213 100644 --- a/repos/effect/packages/effect/CONFIG.md +++ b/repos/effect/packages/effect/CONFIG.md @@ -17,7 +17,7 @@ The simplest case: read one value from an environment variable. import { Config, Effect } from "effect" const program = Effect.gen(function*() { - const host = yield* Config.string("HOST") + const host = yield* Config.String("HOST") console.log(host) }) @@ -35,8 +35,8 @@ Use `Config.all` to group related keys: import { Config, ConfigProvider, Effect } from "effect" const dbConfig = Config.all({ - host: Config.string("host"), - port: Config.int("port") + host: Config.String("host"), + port: Config.Int("port") }) const provider = ConfigProvider.fromUnknown({ @@ -77,25 +77,28 @@ The schema automatically decodes raw string values into their target types. For ## Config Constructors -Each constructor reads a single value and decodes it into the appropriate type. - -| Constructor | Decoded type | Notes | -| ------------------------------ | ------------------ | ------------------------------------------------------------------------ | -| `Config.string(name?)` | `string` | Any string | -| `Config.nonEmptyString(name?)` | `string` | Rejects `""` | -| `Config.number(name?)` | `number` | Includes `NaN`, `Infinity` | -| `Config.finite(name?)` | `number` | Rejects `NaN` and `Infinity` | -| `Config.int(name?)` | `number` | Integers only | -| `Config.boolean(name?)` | `boolean` | Accepts `true/false`, `yes/no`, `on/off`, `1/0`, `y/n` | -| `Config.port(name?)` | `number` | Integer in 1–65535 | -| `Config.url(name?)` | `URL` | Parsed via the `URL` constructor | -| `Config.date(name?)` | `Date` | Rejects invalid dates | -| `Config.duration(name?)` | `Duration` | Parses `"10 seconds"`, `"500 millis"`, `"Infinity"`, `"-Infinity"`, etc. | -| `Config.logLevel(name?)` | `string` | One of `All`, `Fatal`, `Error`, `Warn`, `Info`, `Debug`, `Trace`, `None` | -| `Config.redacted(name?)` | `Redacted` | Hidden from logs and `toString` | -| `Config.literal(value, name?)` | literal type | Accepts only the given literal | - -The optional `name` parameter sets the local path segment for lookup. If the config is wrapped with `Config.nested`, the nested prefix is prepended to this local path. Omit `name` when the config should decode the provider root. +Each constructor reads and decodes a configuration value 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 | +| `Config.Literals(values, name?)` | literal union | Accepts one of the given literals | +| `Config.Array(value, ...)` | `ReadonlyArray` | Accepts structural arrays and flat separated strings | +| `Config.Record(key, value, ...)` | `Record` | Accepts structural records and flat separated key-value strings | + +The optional `name` parameter sets the local path segment for lookup. If the config is wrapped with `Config.nested`, the nested prefix is prepended to this local path. Omit `name` when the config should decode the provider root. `Config.Array` and `Config.Record` additionally accept an options object directly when no path is needed, or a path followed by the options object. ### Parsing and Path Ownership @@ -117,7 +120,7 @@ Triggers when the config cannot resolve and none of its relevant provider input ```ts import { Config, ConfigProvider, Effect } from "effect" -const port = Config.int("port").pipe(Config.withDefault(3000)) +const port = Config.Int("port").pipe(Config.withDefault(3000)) const provider = ConfigProvider.fromUnknown({}) Effect.runSync(port.parse(provider)) // 3000 @@ -130,7 +133,7 @@ Returns `Option.some(value)` on success and `Option.none()` when the config is a ```ts import { Config, ConfigProvider, Effect } from "effect" -const maybePort = Config.option(Config.int("port")) +const maybePort = Config.option(Config.Int("port")) const provider = ConfigProvider.fromUnknown({}) Effect.runSync(maybePort.parse(provider)) // { _tag: "None" } @@ -141,7 +144,7 @@ Effect.runSync(maybePort.parse(provider)) // { _tag: "None" } ```ts import { Config } from "effect" -const upperHost = Config.string("HOST").pipe( +const upperHost = Config.String("HOST").pipe( Config.map((s) => s.toUpperCase()) ) ``` @@ -153,7 +156,7 @@ Unlike `withDefault`, this catches **all** `ConfigError`s: ```ts import { Config } from "effect" -const host = Config.string("HOST").pipe( +const host = Config.String("HOST").pipe( Config.orElse(() => Config.succeed("localhost")) ) ``` @@ -166,8 +169,8 @@ Prepends a logical path segment to every key the inner config reads. The prefix import { Config, ConfigProvider, Effect } from "effect" const dbConfig = Config.all({ - host: Config.string("host"), - port: Config.int("port") + host: Config.String("host"), + port: Config.Int("port") }).pipe(Config.nested("database")) const provider = ConfigProvider.fromUnknown({ @@ -183,7 +186,7 @@ With environment variables, nesting uses `_` as separator: ```ts import { Config, ConfigProvider, Effect } from "effect" -const host = Config.string("host").pipe(Config.nested("database")) +const host = Config.String("host").pipe(Config.nested("database")) const provider = ConfigProvider.fromEnv({ env: { database_host: "localhost" } @@ -197,7 +200,7 @@ Multiple `Config.nested` calls compose with the outermost prefix first: ```ts import { Config, ConfigProvider, Effect } from "effect" -const config = Config.string("host").pipe( +const config = Config.String("host").pipe( Config.nested("database"), Config.nested("production") ) @@ -222,13 +225,13 @@ import { Config } from "effect" // As a record const appConfig = Config.all({ - host: Config.string("host"), - port: Config.int("port"), - debug: Config.boolean("debug") + 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")]) +const pair = Config.all([Config.String("a"), Config.Int("b")]) ``` For example, providing only `host` is an error here: @@ -237,8 +240,8 @@ For example, providing only `host` is an error here: import { Config } from "effect" const database = Config.all({ - host: Config.string("host"), - port: Config.int("port") + host: Config.String("host"), + port: Config.Int("port") }).pipe( Config.withDefault({ host: "localhost", port: 5432 }) ) @@ -248,8 +251,8 @@ The default applies when both keys are absent, but not when only one key is pres ```ts const listener = Config.all({ - host: Config.string("host"), - port: Config.int("port").pipe(Config.withDefault(8080)) + host: Config.String("host"), + port: Config.Int("port").pipe(Config.withDefault(8080)) }).pipe(Config.option) ``` @@ -287,28 +290,29 @@ At the lookup path of a `Config.schema`, an unavailable representation is passed This keeps the provider responsible only for reporting what exists. Schema remains responsible for deciding whether the loaded representation is valid. -Plain `Schema.Array` and `Schema.Record` accept structural provider input only. Use `Config.Array` for separated scalar input such as `"a,b,c"`, and `Config.Record` for input such as `"a=1,b=2"`. +Plain `Schema.Array` and `Schema.Record` accept structural provider input only. Use `Config.Array(Schema.String, "items")` for separated scalar input such as `"a,b,c"`, and `Config.Record(Schema.String, Schema.String, "items")` for input such as `"a=1,b=2"`. Both constructors also accept structural input. The canonical `StringTree` encoding must expose a concrete scalar, object, array, or union shape. `Config.schema` rejects opaque encodings such as `Schema.Any`, `Schema.Unknown`, `Schema.ObjectKeyword`, `Schema.Json`, and `Schema.MutableJson` synchronously when the config is constructed, including when they are nested in another schema. Suspended recursive schemas and declarations such as `Schema.URL` remain supported when their eventual canonical encoding has a concrete shape. To read arbitrary JSON from one scalar provider value, use `Schema.fromJsonString(Schema.Json)`. ### Custom Config Logic -There is no public low-level `Config.make` constructor. For custom validation or transformation, start from one of the public constructors or `Config.schema`, then use `Config.map`, `Config.mapOrFail`, `Config.all`, `Config.orElse`, or `Config.withDefault`. +There is no public low-level `Config.make` constructor. For custom validation or transformation, start from one of the public constructors or `Config.schema`, then use `Config.map`, `Config.mapEffect`, `Config.all`, `Config.orElse`, or `Config.withDefault`. If you need custom lookup behavior for a new backing source, implement a `ConfigProvider` with `ConfigProvider.make` instead. -## Config Schemas +## Array and Record Constructors -For reusable codecs you can pass directly to `Config.schema`: +`Config.Array` accepts `{ separator? }`, while `Config.Record` accepts `{ separator?, keyValueSeparator? }`. Pass the options object directly to read the provider root, or place a string or `ConfigProvider.Path` before it to select a path: -| 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.Array(value)` | `Array` | Also parses flat `"v1,v2"` strings | -| `Config.Record(key, value)` | `Record` | Also parses flat `"k1=v1,k2=v2"` strings | +```ts +import { Config, Schema } from "effect" + +const rootValues = Config.Array(Schema.String, { separator: ";" }) +const namedValues = Config.Array(Schema.String, "VALUES", { separator: ";" }) + +const rootHeaders = Config.Record(Schema.String, Schema.String, { keyValueSeparator: ":" }) +const namedHeaders = Config.Record(Schema.String, Schema.String, "HEADERS", { keyValueSeparator: ":" }) +``` ## ConfigProvider Sources @@ -338,7 +342,7 @@ const provider = ConfigProvider.fromEnv({ } }) -const host = Config.string("HOST").parse( +const host = Config.String("HOST").parse( provider.pipe(ConfigProvider.nested("DATABASE")) ) @@ -638,7 +642,7 @@ const TestLayer = ConfigProvider.layer( ) const program = Effect.gen(function*() { - const port = yield* Config.int("port") + const port = yield* Config.Int("port") return port }) @@ -673,7 +677,7 @@ import { Config, ConfigProvider, Effect } from "effect" const provider = ConfigProvider.fromUnknown({ HOST: "localhost" }) const program = Effect.gen(function*() { - const host = yield* Config.string("HOST") + const host = yield* Config.String("HOST") return host }).pipe( Effect.provideService(ConfigProvider.ConfigProvider, provider) @@ -686,14 +690,14 @@ const program = Effect.gen(function*() { ```ts const program = Effect.gen(function*() { - const host = yield* Config.string("HOST") + 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 host = Config.String("HOST") const result = Effect.runSync(host.parse(provider)) ``` @@ -711,7 +715,7 @@ Check `error.cause._tag` to distinguish: ```ts import { Config, ConfigProvider, Effect } from "effect" -const program = Config.int("PORT").parse( +const program = Config.Int("PORT").parse( ConfigProvider.fromUnknown({ PORT: "not-a-number" }) ).pipe( Effect.tapError((error) => @@ -754,7 +758,7 @@ const DbConfig = Config.schema( const AppConfig = Config.all({ server: ServerConfig, db: DbConfig, - debug: Config.boolean("debug").pipe(Config.withDefault(false)) + debug: Config.Boolean("debug").pipe(Config.withDefault(false)) }) // In production, just yield it — reads from process.env diff --git a/repos/effect/packages/effect/MCP.md b/repos/effect/packages/effect/MCP.md index 66258490c0..53f77d24fc 100644 --- a/repos/effect/packages/effect/MCP.md +++ b/repos/effect/packages/effect/MCP.md @@ -9,17 +9,16 @@ It's important to understand the architecture of the Effect MCP server. Here is an example of a MCP server implementation: ```typescript -import { NodeRuntime, NodeSink, NodeStream } from "@effect/platform-node" -import { Effect, Layer, Logger } from "effect" -import { Schema } from "effect/schema" +import { NodeRuntime, NodeStdio } from "@effect/platform-node" +import { Effect, Layer, Logger, Schema } from "effect" import { McpProtocol, McpServer, Tool, Toolkit } from "effect/unstable/ai" // Define a simple tool const DemoTool = Tool.make("DemoTool", { description: "A demo tool that echoes back the input", - parameters: { + parameters: Schema.Struct({ message: Schema.String - }, + }), success: Schema.String }) @@ -58,12 +57,12 @@ const ServerLayer = Layer.mergeAll( McpServer.layerStdio({ name: "Demo MCP Server", version: "1.0.0", - protocols: [McpProtocol.v2025_06_18], - stdin: NodeStream.stdin, - stdout: NodeSink.stdout + protocols: [McpProtocol.v2025_06_18] }) ), - Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })])) + Layer.provide(NodeStdio.layer), + Layer.provide(Logger.layer([Logger.consolePretty()])), + Layer.provideMerge(Layer.succeed(Logger.LogToStderr, true)) ) Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) @@ -99,8 +98,7 @@ resource is defined as a template that specifies its location, behavior, and met parameters, completions, and content generation. ```typescript -import { Effect } from "effect" -import { Schema } from "effect/schema" +import { Effect, Schema } from "effect" import { McpSchema, McpServer } from "effect/unstable/ai" const SimpleResource = McpServer.resource({ @@ -141,8 +139,7 @@ structured, parameterized instructions or messages that the client can send to t generation logic in a declarative way. ```typescript -import { Effect } from "effect" -import { Schema } from "effect/schema" +import { Effect, Schema } from "effect" import { McpServer } from "effect/unstable/ai" const DemoPrompt = McpServer.prompt({ @@ -169,24 +166,23 @@ contract while the actual logic is provided separately through an implementation grouped into toolkits, which can be combined and converted into layers. ```typescript -import { Effect, Layer } from "effect" -import { Schema } from "effect/schema" +import { Effect, Layer, Schema } from "effect" import { McpServer, Tool, Toolkit } from "effect/unstable/ai" const DemoTool = Tool.make("DemoTool", { description: "This is a demo tool for the documentation", - parameters: { + parameters: Schema.Struct({ demoId: Schema.Number, demoName: Schema.String - }, + }), success: Schema.String }) const OtherDemoTool = Tool.make("OtherDemoTool", { description: "Another demo tool", - parameters: { + parameters: Schema.Struct({ value: Schema.Number - }, + }), success: Schema.String }) @@ -217,8 +213,7 @@ defines both the message shown to the user and the expected response schema, ens validated user input. ```typescript -import { Effect } from "effect" -import { Schema } from "effect/schema" +import { Effect, Schema } from "effect" import { McpServer } from "effect/unstable/ai" const DemoElicitation = McpServer.elicit({ @@ -369,7 +364,7 @@ const ServerLayer = Layer.mergeAll( }) ), Layer.provide(NodeStdio.layer), - Layer.provide(Layer.succeed(Logger.LogToStderr)(true)) + Layer.provideMerge(Layer.succeed(Logger.LogToStderr, true)) ) // Run the server diff --git a/repos/effect/packages/effect/SCHEMA.md b/repos/effect/packages/effect/SCHEMA.md index f52dbed391..3f8b28edcd 100644 --- a/repos/effect/packages/effect/SCHEMA.md +++ b/repos/effect/packages/effect/SCHEMA.md @@ -286,6 +286,10 @@ You can use `Schema.TemplateLiteral` to define structured string patterns made o Template literal matching is based on the semantics of each part rather than only a generated regular expression. Checks on string, number, and bigint schema parts are applied while matching each segment. +Parts must not contain encodings. Construction throws for transformed parts, including transformations inside unions and transformations whose decoded and encoded types are equal. Brands and supported checks without encodings remain valid. Use `Schema.TemplateLiteralParser` when the parts need to decode values, such as `BooleanFromBit` or `FiniteFromString`. + +To describe bit spellings directly, use `Schema.Literals([0, 1])` as the part. To describe finite numeric spellings, use `Schema.Finite`. Replacing `FiniteFromString` with `Finite` changes the accepted spelling rules: a finite numeric part does not accept an empty segment. Explicit `Schema.toType` and `Schema.toEncoded` projections remove transformations, but can also change the constraints a template validates. + **Example** (Constraining parts of an email-like string) ```ts @@ -322,6 +326,12 @@ Failure(Cause([Fail(SchemaError(Expected a string matching template literal part If you want to extract the parts of a string that match a template, you can use `Schema.TemplateLiteralParser`. This allows you to parse the input into its individual components rather than treat it as a single string. +The parser transforms a template built from the encoded sides of the parts into a tuple that retains their decoders and checks. Encoding applies the parts' encoders and joins the segments. The parser requires the decoding and encoding services of its parts in the corresponding direction. + +`Schema.toEncoded(parser)` validates that source template. Use `Schema.String` if you need to accept unrestricted strings. + +Ambiguous templates use greedy segmentation with backtracking. Encoding a tuple and decoding the resulting string can produce a different tuple when a segment contains a separator used by the template. + **Example** (Parsing a template literal into components) ```ts @@ -952,7 +962,7 @@ type Encoded = { type Encoded = typeof schema.Encoded ``` -If you want the record part to be mutable, you can wrap it in `Schema.mutable`. +If you want the record part to be mutable, apply `Schema.mutableKey` to its value schema. **Example** (Allowing dynamic keys to be mutable) @@ -1630,6 +1640,14 @@ const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapE An array schema describes a variable-length list where every element shares the same type. +### Mutability + +Array and tuple schemas are readonly by default. Use `Schema.mutable` to make them mutable. + +> [!NOTE] +> `Schema.mutable` does not support an encoding attached directly to the array or tuple schema. Apply it before adding +> such an encoding. Encodings on element schemas are supported. + ### Unique Arrays You can deduplicate arrays using `Schema.UniqueArray`. @@ -5561,371 +5579,6 @@ console.log(JSON.stringify(document, null, 2)) */ ``` -### Generating an Arbitrary from a Schema - -Property-based tests need generators. `Schema.toArbitrary` derives a factory -that accepts the `fast-check` module and returns an `Arbitrary` that generates -decoded `Type` values accepted by the schema. - -Most schemas do not need any extra work: - -```ts -import { Schema } from "effect" -import { FastCheck } from "effect/testing" - -const Person = Schema.Struct({ - name: Schema.String, - age: Schema.Int.check(Schema.isBetween({ minimum: 18, maximum: 80 })) -}) - -const PersonArbitrary = Schema.toArbitrary(Person)(FastCheck) - -console.log(FastCheck.sample(PersonArbitrary, 3)) -``` - -`Schema.Never` and declaration schemas without a `toArbitrary` annotation cannot -be derived automatically. - -#### Filters - -Generated values are always checked by the schema filters before they are -returned. The important question is whether a filter can also help choose a good -generator. - -Built-in filters already do this: - -```ts -import { Schema } from "effect" - -const Username = Schema.String.check( - Schema.isMinLength(3), - Schema.isMaxLength(20), - Schema.isPattern(/^[a-z0-9_]+$/) -) - -const PositiveInteger = Schema.Int.check( - Schema.isGreaterThanOrEqualTo(1) -) - -const Tags = Schema.Array(Schema.String).check( - Schema.isMinLength(1), - Schema.isUnique() -) -``` - -For these schemas, `toArbitrary` does not generate random unconstrained strings, -numbers, or arrays and then hope the filters pass. It uses the length, range, -pattern, and uniqueness metadata to build a better generator first. - -A custom filter without metadata is still correct, but may be inefficient: - -```ts -import { Schema } from "effect" - -const isPalindrome = (s: string) => s === Array.from(s).reverse().join("") - -const Palindrome = Schema.String.check( - Schema.makeFilter(isPalindrome, { - expected: "a palindrome" - }) -) -``` - -This works because the final predicate check rejects strings that are not -palindromes. It may need many attempts, because the base string generator has no -reason to produce mirrored strings. - -#### Custom Filters With Constraints - -If part of a custom filter can be described as a normal generation constraint, -attach `arbitrary.constraint` to the filter. The constraint does not have to -prove the whole predicate; it just makes the base generator closer to the values -the predicate accepts. - -```ts -import { Order, Schema } from "effect" - -const isPrimeNumber = (n: number) => { - if (!Number.isInteger(n) || n < 2) { - return false - } - for (let divisor = 2; divisor * divisor <= n; divisor++) { - if (n % divisor === 0) { - return false - } - } - return true -} - -const prime = Schema.makeFilter(isPrimeNumber, { - expected: "a prime number", - arbitrary: { - constraint: { - integer: true, - ordered: { - order: Order.Number, - minimum: 2 - } - } - } -}) - -const Prime = Schema.Number.check(prime) -``` - -The filter still checks primality. The constraint only tells `toArbitrary` not -to waste time on non-integers or numbers below `2`. - -Think of `constraint` as a small vocabulary that the current schema node can -understand: - -- On strings, `minLength` and `maxLength` mean string length. -- On arrays, `minLength` and `maxLength` mean array length. -- On objects, `minLength` and `maxLength` mean final own-property count. -- On sets, maps, hash collections, and chunks, `minLength` and `maxLength` mean final collection size. -- `patterns` apply to string generation. -- `integer`, `noNaN`, `noInfinity`, `valid`, and `unique` are enabled when any contributing filter sets them. -- `ordered` stores bounds for ordered values such as numbers, bigints, dates, `DateTime`, and `BigDecimal`. - -Fields that do not make sense for the current node are ignored. The final filter -check still validates every generated value. - -#### Custom Filters With Candidates - -Use a candidate when the filter cannot be expressed with the constraint -vocabulary. - -```ts -import { Schema } from "effect" - -const reverse = (s: string) => Array.from(s).reverse().join("") - -const isPalindrome = (s: string) => s === reverse(s) - -const palindrome = Schema.makeFilter( - isPalindrome, - { - expected: "a palindrome", - arbitrary: { - candidate: { - weight: 5, - make: (fc) => fc.string().map((half) => `${half}${reverse(half)}`) - } - } - } -) - -const Palindrome = Schema.String.check(palindrome) -``` - -A candidate is an extra source used together with the schema node's base -generator. The base generator has weight `1`. A candidate has weight `1` unless -you set another positive integer weight. - -With one candidate at weight `5`, fast-check tries the candidate roughly five -times as often as the base generator. Candidate values are still checked by all -filters, so a bad candidate can waste attempts but cannot produce invalid -values. - -`make` receives the arbitrary context and may return `undefined` when the -candidate should not be used for that context. - -#### Schema-Level Overrides - -Use a `toArbitrary` annotation when you want to replace the generator for a -schema node. - -The annotation is not limited to declaration schemas. You can attach it to a -normal schema with `.annotate(...)`: - -```ts -import { Schema } from "effect" - -const Name = Schema.String.annotate({ - toArbitrary: () => (fc) => fc.constantFrom("Alice", "Bob", "Carol") -}) -``` - -Put override annotations on base schemas when possible, before adding filters: - -```ts -const Name = Schema.String.annotate({ - toArbitrary: () => (fc) => fc.constantFrom("Alice", "Bob", "Carol") -}).check(Schema.isMinLength(1)) -``` - -This shape is easier to reason about. The override provides the base generator; -the filter remains a normal filter. Schema still checks generated values at the -end. - -Avoid putting an override on a schema that already has filters unless the -override intentionally handles those filters too: - -```ts -const Name = Schema.String.check(Schema.isMinLength(1)).annotate({ - toArbitrary: () => (fc) => fc.constant("") -}) -``` - -This is valid TypeScript, but it is a bad generator: it always generates a value -that the filter rejects. - -The second argument of a `toArbitrary` hook is the arbitrary context. Its -`constraint` field contains constraints collected from filters on the same -schema node as the override. If the override is placed before `.check(...)`, the -context does not include the later filters. If the override is placed after -`.check(...)`, the context includes those filters and the override must respect -them. - -`context.recursion` is present while deriving inside a recursive schema. - -#### Declaration Schemas - -Declaration schemas are opaque to Schema. If you define one, provide a -`toArbitrary` hook. - -For an atomic declaration, return a normal `fast-check` arbitrary: - -```ts -import { Schema } from "effect" - -const Url = Schema.instanceOf(globalThis.URL, { - title: "URL", - toArbitrary: () => (fc) => fc.webUrl().map((s) => new globalThis.URL(s)) -}) -``` - -Generic declarations receive one derivation per type parameter: - -- `arbitrary`: the normal generator for the type parameter. -- `terminal`: a finite generator for the type parameter, used to close recursive generation. - -For an opaque wrapper type, you usually map both sources in the same way: - -```ts -import { Effect, Schema, SchemaIssue, SchemaParser } from "effect" - -class Box { - private constructor(private readonly value: A) {} - - static make(value: A): Box { - return new Box(value) - } - - static unbox(box: Box): A { - return box.value - } -} - -const isBox = (u: unknown): u is Box => u instanceof Box - -const BoxSchema = (value: A) => - Schema.declareConstructor, Box>()( - [value], - ([valueCodec]) => (input, ast, options) => { - if (!isBox(input)) { - return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) - } - return Effect.map( - SchemaParser.decodeUnknownEffect(valueCodec)(Box.unbox(input), options), - Box.make - ) - }, - { - toArbitrary: ([value]) => () => ({ - arbitrary: value.arbitrary.map(Box.make), - terminal: value.terminal?.map(Box.make) - }) - } - ) -``` - -This looks like duplicated code, but it is not the same generator twice. It is -the same opaque constructor applied to two different sources. - -Suppose someone later builds a recursive schema like this: - -```ts -interface Tree { - readonly value: A - readonly children: ReadonlyArray> -} - -type BoxedTree = Box> -``` - -`Box` does not know whether `A` is recursive. If `A` is `Tree`, then -`value.arbitrary` may generate a recursive tree, while `value.terminal` is the -finite tree generator used when the recursion budget is exhausted. Mapping both -sources through `Box.make` preserves that information. If `Box` returned only -`arbitrary`, it would hide the finite path from outer recursive schemas. - -If the type parameter has no finite terminal generator, `value.terminal` is -`undefined`, and the wrapper cannot provide a terminal branch either. - -#### Integration with Synthetic Data Generation Tools - -Synthetic data libraries such as `@faker-js/faker` are useful when the generated -values should look realistic. Put them behind a Fast-Check arbitrary instead of -calling them directly, so Fast-Check still controls randomness and shrinking. - -```ts -import { faker } from "@faker-js/faker" -import { Schema } from "effect" -import { FastCheck } from "effect/testing" - -/** - * Make it easy to plug a Faker generator into a Schema's `toArbitrary` override. - * The seed comes from Fast-Check so data is reproducible and shrinks correctly. - */ -function fake( - gen: (f: typeof faker) => A -): Schema.Annotations.ToArbitrary.Declaration { - return () => (fc) => - fc.nat().map((seed) => { - faker.seed(seed) - return gen(faker) - }) -} - -const FirstName = Schema.String.annotate({ - toArbitrary: fake((faker) => faker.person.firstName()) -}) - -const LastName = Schema.String.annotate({ - toArbitrary: fake((faker) => faker.person.lastName()) -}) - -const JobTitle = Schema.String.annotate({ - toArbitrary: fake((faker) => faker.person.jobTitle()) -}) - -const Company = Schema.String.annotate({ - toArbitrary: fake((faker) => faker.company.name()) -}) - -const Person = Schema.Struct({ - firstName: FirstName, - lastName: LastName, - jobTitle: JobTitle, - company: Company -}) - -console.log(FastCheck.sample(Schema.toArbitrary(Person)(FastCheck), 3)) -``` - -These overrides are useful because the values have domain shape: names look like -names, job titles look like job titles, and companies look like companies. For -plain numeric ranges, prefer Schema constraints and the default arbitrary -derivation. - -If you combine a Faker source with filters, put the override on the base schema -first and add filters afterwards. This keeps the responsibilities simple: the -override chooses a realistic source, and the filter remains the final validation -rule. If you put the override after `.check(...)`, the override must respect -those filters itself, or generation will spend time producing values that are -rejected. - ### Generating an Equivalence from a Schema An equivalence function checks whether two values are structurally equal according to the schema's definition. Schema derives this automatically, so you do not need to write manual comparison logic. @@ -6366,7 +6019,7 @@ const json = SchemaRepresentation.toJson( const document = SchemaRepresentation.fromJson(json) const rebuilt = SchemaRepresentation.fromRepresentation(document, { - revivers: [Schema.isMinLengthReviver] + revivers: [SchemaRepresentation.isMinLengthReviver] }) console.log(Schema.is(rebuilt)("abc")) @@ -6375,9 +6028,9 @@ console.log(Schema.is(rebuilt)("a")) // false ``` -Effect exports individual revivers next to the built-in declarations and checks they reconstruct, such as -`Schema.OptionReviver`, `Schema.DateReviver`, and `Schema.isMinLengthReviver`. Supply every reviver required by the -document; a missing or duplicate `id`, or a payload that does not satisfy its reviver's `payloadSchema`, is an error. +`SchemaRepresentation` exports individual revivers for built-in declarations and checks, such as +`OptionReviver`, `DateReviver`, and `isMinLengthReviver`. Supply every reviver required by the document; a missing or +duplicate `id`, or a payload that does not satisfy its reviver's `payloadSchema`, is an error. `fromRepresentations` rebuilds the ordered roots of a `MultiDocument` in a shared reference environment. Only references reachable from those roots are revived. @@ -6390,7 +6043,7 @@ There are separate reviver contracts for opaque declarations, leaf filters, and - `FilterReviver

` - `FilterGroupReviver

` -Use `makeDeclarationReviver`, `makeFilterReviver`, and `makeFilterGroupReviver` to infer `P` from `payloadSchema`. +Use `makeReviverDeclaration`, `makeReviverFilter`, and `makeReviverFilterGroup` to infer `P` from `payloadSchema`. ```ts import { Schema, SchemaRepresentation } from "effect" @@ -6407,7 +6060,7 @@ function minLength( }) } -const minLengthReviver = SchemaRepresentation.makeFilterReviver( +const minLengthReviver = SchemaRepresentation.makeReviverFilter( id, Schema.Struct({ minimum: Schema.Number }), ({ annotations, payload }) => minLength(payload.minimum, annotations) diff --git a/repos/effect/packages/effect/benchmark/http/serverAllocations.ts b/repos/effect/packages/effect/benchmark/http/serverAllocations.ts new file mode 100644 index 0000000000..927a0cac53 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/http/serverAllocations.ts @@ -0,0 +1,138 @@ +import { HttpRouter, HttpServerResponse } from "effect/unstable/http" +import * as inspector from "node:inspector/promises" + +const WARMUP = 5_000 +const REQUESTS = 50_000 +const RETAINED_LIMIT = 64 +const CHURN_LIMIT = 17_000 +const DEFERRED_REQUESTS = 20_000 +const DEFERRED_CONCURRENCY = 32 +// main deferred a per-request span-end task via setImmediate (~4k bytes/request +// held until the loop turns); the native-tracer fast path ends the span inline. +const DEFERRED_LIMIT = 512 + +const gc = (globalThis as any).gc as undefined | (() => void) +if (gc === undefined) { + console.error("run with --expose-gc") + process.exit(1) +} + +const session = new inspector.Session() +session.connect() + +const { dispose, handler } = HttpRouter.toWebHandler( + HttpRouter.add("GET", "/ping", HttpServerResponse.text("pong")), + { disableLogger: true } +) + +const run = async (count: number) => { + for (let i = 0; i < count; i++) { + const response = await handler(new Request("http://localhost/ping")) + await response.arrayBuffer() + } +} + +// a promise-chained load never yields to the event loop, so any per-request +// macrotask (setImmediate) the request path schedules accumulates unrun +const runConcurrent = async (count: number) => { + let remaining = count + const worker = async () => { + while (remaining > 0) { + remaining-- + const response = await handler(new Request("http://localhost/ping")) + await response.arrayBuffer() + } + } + await Promise.all(Array.from({ length: DEFERRED_CONCURRENCY }, worker)) +} + +interface ProfileNode { + callFrame: { functionName: string; url: string; lineNumber: number } + selfSize: number + children?: Array +} + +const topFrames = (head: ProfileNode) => { + const byFrame = new Map() + let total = 0 + const visit = (node: ProfileNode) => { + if (node.selfSize > 0) { + const f = node.callFrame + const url = f.url.replace(/^.*\/(packages|node_modules)\//, "$1/") + const key = `${f.functionName || "(anonymous)"} @ ${url}:${f.lineNumber + 1}` + byFrame.set(key, (byFrame.get(key) ?? 0) + node.selfSize) + total += node.selfSize + } + node.children?.forEach(visit) + } + visit(head) + return { + total, + frames: [...byFrame.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15) + } +} + +// Allow FinalizationRegistry callbacks to run before measuring retained heap. +const heapUsed = async () => { + for (let i = 0; i < 5; i++) { + gc() + await new Promise((resolve) => setImmediate(resolve)) + } + return process.memoryUsage().heapUsed +} + +await run(WARMUP) + +const heapBefore = await heapUsed() +await run(REQUESTS) +const heapAfter = await heapUsed() + +// A promise-chained burst never yields to the event loop, so synchronous-GC +// heap deltas expose per-request work deferred into macrotask queues. +await runConcurrent(WARMUP) +gc() +gc() +const deferredBefore = process.memoryUsage().heapUsed +await runConcurrent(DEFERRED_REQUESTS) +gc() +gc() +const deferredAfter = process.memoryUsage().heapUsed + +await session.post("HeapProfiler.startSampling", { + samplingInterval: 16384, + includeObjectsCollectedByMajorGC: true, + includeObjectsCollectedByMinorGC: true +}) +await run(REQUESTS) +const { profile } = await session.post("HeapProfiler.stopSampling") + +await dispose() + +const { frames, total } = topFrames(profile.head as ProfileNode) +const churnPerRequest = Math.round(total / REQUESTS) +const retainedPerRequest = Math.round((heapAfter - heapBefore) / REQUESTS) +const deferredPerRequest = Math.round((deferredAfter - deferredBefore) / DEFERRED_REQUESTS) + +console.log(`requests: ${REQUESTS}`) +console.log(`allocated/request: ${churnPerRequest} bytes (limit ${CHURN_LIMIT})`) +console.log(`retained/request: ${retainedPerRequest} bytes (limit ${RETAINED_LIMIT})`) +console.log(`deferred/request: ${deferredPerRequest} bytes (limit ${DEFERRED_LIMIT})`) +console.log("top allocation sites:") +for (const [key, size] of frames) { + const pct = ((size / total) * 100).toFixed(1).padStart(5) + console.log(` ${pct}% ${(size / 1024 / 1024).toFixed(2).padStart(7)} MiB ${key}`) +} + +if (retainedPerRequest > RETAINED_LIMIT) { + console.error(`FAIL: retained heap grows ${retainedPerRequest} bytes/request`) + process.exit(1) +} +if (deferredPerRequest > DEFERRED_LIMIT) { + console.error(`FAIL: microtask-only burst holds ${deferredPerRequest} bytes/request exceeds ${DEFERRED_LIMIT}`) + process.exit(1) +} +if (churnPerRequest > CHURN_LIMIT) { + console.error(`FAIL: allocation churn ${churnPerRequest} bytes/request exceeds ${CHURN_LIMIT}`) + process.exit(1) +} +console.log("PASS") diff --git a/repos/effect/packages/effect/benchmark/rpc/RpcSerialization.ts b/repos/effect/packages/effect/benchmark/rpc/RpcSerialization.ts index f5bc5e0470..e0665b14d9 100644 --- a/repos/effect/packages/effect/benchmark/rpc/RpcSerialization.ts +++ b/repos/effect/packages/effect/benchmark/rpc/RpcSerialization.ts @@ -136,7 +136,7 @@ const schemaBinary = Effect.runSync( ) const formats = [ - { name: "Msgpack", serialization: RpcSerialization.msgPack }, + { name: "NDJSON", serialization: RpcSerialization.ndjson }, { name: "SchemaBinary", serialization: schemaBinary } ] as const @@ -210,9 +210,7 @@ console.log(`${process.platform} ${process.arch}; ${cpus()[0]?.model ?? "unknown console.log( "End-to-end operations include the payload codec plus RPC envelope framing; codec construction is excluded." ) -console.log( - "Msgpack uses RpcSerialization.msgPack defaults, including records. SchemaBinary fingerprints envelopes only and shares one string dictionary across the frames of a connection." -) +console.log("SchemaBinary fingerprints envelopes only and keeps every frame independently decodable.") console.log( "First-frame sizes use a fresh serializer; steady sizes and throughput reuse one as on a long-lived connection, and decode walks a stream in frame order." ) @@ -258,11 +256,11 @@ for (const entry of prepared) { await bench.run() assert.notStrictEqual(sink, undefined) -const msgpackThroughput = new Map() +const ndjsonThroughput = new Map() for (const task of bench.tasks) { const label = labels.get(task.name)! - if (label.formatName === "Msgpack" && task.result?.state === "completed") { - msgpackThroughput.set(`${label.caseName}/${label.direction}`, task.result.throughput.mean) + if (label.formatName === "NDJSON" && task.result?.state === "completed") { + ndjsonThroughput.set(`${label.caseName}/${label.direction}`, task.result.throughput.mean) } } @@ -285,13 +283,13 @@ console.table(bench.tasks.map((task) => { State: result?.state ?? "missing result" } } - const baseline = msgpackThroughput.get(`${label.caseName}/${label.direction}`)! + const baseline = ndjsonThroughput.get(`${label.caseName}/${label.direction}`)! return { Case: label.caseName, Format: label.formatName, Direction: label.direction, "Throughput avg (ops/s)": Math.round(result.throughput.mean), - "vs Msgpack": `${(result.throughput.mean / baseline).toFixed(2)}x`, + "vs NDJSON": `${(result.throughput.mean / baseline).toFixed(2)}x`, "Latency med (us/op)": (result.latency.p50 * 1_000).toFixed(2), "Latency RME": `${result.latency.rme.toFixed(2)}%`, Samples: result.latency.samplesCount diff --git a/repos/effect/packages/effect/benchmark/schema/SchemaBinary.md b/repos/effect/packages/effect/benchmark/schema/SchemaBinary.md index a337b718f7..216d074688 100644 --- a/repos/effect/packages/effect/benchmark/schema/SchemaBinary.md +++ b/repos/effect/packages/effect/benchmark/schema/SchemaBinary.md @@ -1,155 +1,13 @@ # SchemaBinary benchmark -Run from the repository root: +Run the benchmark from the repository root: ```sh nix develop -c pnpm --dir packages/effect exec node benchmark/schema/SchemaBinary.ts ``` -To repeat the raw msgpackr cases without its native string extractor: +The benchmark compares SchemaBinary's default and fingerprint modes with JSON and Protobuf. It measures encoded, gzip, and zstd sizes together with one-shot encode/decode throughput. -```sh -nix develop -c env MSGPACKR_NATIVE_ACCELERATION_DISABLED=true pnpm --dir packages/effect exec node benchmark/schema/SchemaBinary.ts -``` - -These results are from one full run with fingerprint-mode row runs on Linux x86_64 (AMD EPYC 4344P) with Node 26.7.0. Codec and schema construction are excluded. One-shot tasks use 100 warmups and 1,000 measured samples; streaming tasks use 25 warmups and 250 measured samples. Throughput is machine-local and should only be compared within this run. - -Both modes pack each field id with a wire kind, so varint numbers, decimals, and booleans skip the per-field length prefix and booleans ride in the tag itself. Short decimals such as `12.5` encode as a varint mantissa plus a scale instead of an eight-byte f64, and index-signature record pairs pack the value kind into the key-length varint. An array of structs is written as a row run: each distinct set of present fields declares its shape once, and later rows reference that shape and any string already written in the same slot. Fingerprint mode omits field identifiers entirely: single frames use positional layouts and its row shapes are presence masks instead of field id lists. JSON, Msgpack, and NDJSON use the same `Schema.toCodecJson` representation. - -Protobuf uses `protobufjs` reflection types parsed once before timing. `Schema.Number` maps to proto3 `double`, records map to `map`, and top-level arrays and records use wrapper messages. Tuple samples and nested arrays use messages because protobuf does not support either shape directly. The timed decode paths include `toObject` and the case adapters, so every format produces its final application value. Descriptor construction and Protobuf encode adapters are excluded. - -Every format is timed through its public API, so the SchemaBinary and JSON / Msgpack numbers include the Schema pass that produces or validates the application value. Where the binary layer already validates a schema on its own, `toCodec` skips that pass in both directions rather than repeating the work: encoding runs the binary encoder directly, and decoding hands the value it just produced straight through. Any input the binary layer did not produce, `Schema.is` included, still runs the real check. - -The raw serializer section deliberately relaxes that rule. It compares the public SchemaBinary codec with raw msgpackr and JSON calls that do not validate the application value. `Effect Msgpack schema` remains in those tables as the public-API comparison. The benchmark prints whether msgpackr's native string extractor is active. - -A static `.proto` must pick one numeric type for `Schema.Number`, so integral values pay eight bytes where SchemaBinary picks a varint per value. Typing the known-integer fields as `uint32` would reduce the 200-row Protobuf payload from 24,480 to 20,600 bytes and the small record from 42 to 35, but would no longer cover the full `Schema.Number` domain. The reported sizes also reflect `protobufjs` 7.6.5 encoding default-valued scalars such as `verified: false`; an encoder that applies proto3 implicit presence would omit them. - -The streaming setup compares the closest public decode paths. SchemaBinary reuses one synchronous parser for each feed shape. Protobuf calls `decodeDelimited` across the batch and materializes every message with `toObject` plus the case adapter. Msgpack synchronously calls `unpackMultiple` on the batch, then validates each value. NDJSON runs `Ndjson.decodeSchema` through Effect Stream and Channel for every operation, including UTF-8 decoding, line splitting, `JSON.parse`, schema validation, and runtime scheduling. A batch is one Channel run over 32 lines, or 200 lines for the per-frame case. Single and fragmented measurements each run a complete Channel for one line, so their scheduling cost is not amortized. This makes batch the closest throughput comparison while preserving the cost of each public API. - -SchemaBinary and Protobuf use length-prefixed frames, NDJSON includes one newline byte per frame, and Msgpack concatenates self-delimiting values. Fragmented inputs split after the first byte. Stream compression is applied to the complete concatenated stream. - -The `200-row array payload` case uses `Schema.Array(LargeRow)`, so one value is an array containing 200 rows. A one-shot operation encodes or decodes that entire array. Its streaming batch contains 32 frames with the same 200-row array, or 6,400 row occurrences in total. The `200 single-row frames` case uses `LargeRow` directly and sends the 200 distinct rows as 200 frames. Streaming throughput is decoded values per second: arrays per second for the first case and rows per second for the second. Multiply the array rate by 200 to compare decoded row throughput. - -## Payload size - -Cells contain raw / gzip -6 / zstd bytes. - -| Case | SchemaBinary | Fingerprint | JSON | Msgpack | Protobuf | -| ---------------------- | -----------------: | -----------------: | ------------------: | ------------------: | ------------------: | -| small record | 47 / 70 / 56 | 30 / 50 / 39 | 89 / 100 / 92 | 69 / 88 / 78 | 42 / 51 / 44 | -| nested payload | 280 / 298 / 290 | 200 / 210 / 209 | 453 / 303 / 309 | 385 / 304 / 299 | 241 / 220 / 214 | -| collections | 1079 / 710 / 687 | 1065 / 692 / 669 | 1828 / 671 / 660 | 1462 / 788 / 805 | 2932 / 799 / 754 | -| index signatures / 128 | 1673 / 646 / 603 | 1680 / 652 / 612 | 2235 / 573 / 559 | 2203 / 676 / 629 | 2834 / 586 / 476 | -| index signatures / 512 | 7145 / 2278 / 2298 | 7152 / 2286 / 2310 | 9531 / 2266 / 2074 | 9287 / 2544 / 2304 | 11666 / 1978 / 1737 | -| 200-row array payload | 7771 / 2441 / 2224 | 7741 / 2398 / 2182 | 57529 / 3230 / 3008 | 51283 / 3516 / 3320 | 24480 / 3111 / 2839 | - -Streaming cells contain total raw / gzip -6 / zstd bytes for the complete stream. - -| Case | Frames | SchemaBinary | Fingerprint | Msgpack | Protobuf | NDJSON | -| ---------------------- | -----: | -------------------: | -------------------: | --------------------: | --------------------: | ---------------------: | -| small record | 32 | 1504 / 83 / 66 | 960 / 60 / 48 | 2240 / 111 / 87 | 1376 / 66 / 51 | 2880 / 123 / 98 | -| nested payload | 32 | 8960 / 380 / 295 | 6400 / 256 / 209 | 10880 / 369 / 306 | 7776 / 271 / 220 | 14528 / 399 / 315 | -| collections | 32 | 34528 / 968 / 729 | 34080 / 935 / 711 | 47424 / 1130 / 813 | 93888 / 2074 / 774 | 58528 / 1079 / 676 | -| index signatures / 128 | 32 | 53536 / 1007 / 615 | 53760 / 1017 / 624 | 71008 / 1252 / 649 | 90752 / 1503 / 492 | 71552 / 1073 / 540 | -| index signatures / 512 | 32 | 228640 / 4538 / 2320 | 228864 / 4557 / 2332 | 297696 / 5775 / 2037 | 373376 / 5956 / 1782 | 305024 / 5831 / 1829 | -| 200-row array payload | 32 | 248672 / 4598 / 2261 | 247712 / 4547 / 2220 | 623488 / 12335 / 2736 | 783456 / 12374 / 2909 | 1840960 / 87481 / 3226 | -| 200 single-row frames | 200 | 27040 / 3079 / 3173 | 21240 / 2876 / 2731 | 51520 / 2956 / 2888 | 24280 / 3104 / 2788 | 57528 / 3230 / 3019 | - -## One-shot throughput - -Average encode operations per second: - -| Case | Default | Fingerprint | JSON | Msgpack | Protobuf | -| ---------------------- | --------: | ----------: | ------: | ------: | --------: | -| small record | 1,319,592 | 1,507,529 | 436,487 | 704,398 | 2,000,847 | -| nested payload | 588,799 | 641,251 | 259,634 | 285,917 | 562,044 | -| collections | 146,285 | 153,447 | 22,636 | 23,006 | 75,809 | -| index signatures / 128 | 138,629 | 139,601 | 36,497 | 35,532 | 63,231 | -| index signatures / 512 | 36,813 | 36,699 | 6,999 | 6,358 | 15,699 | -| 200-row array payload | 19,622 | 19,671 | 7,182 | 4,192 | 12,040 | - -Average decode operations per second: - -| Case | Default | Fingerprint | JSON | Msgpack | Protobuf | -| ---------------------- | --------: | ----------: | ------: | ------: | --------: | -| small record | 1,560,001 | 1,613,600 | 398,999 | 733,207 | 2,281,446 | -| nested payload | 586,239 | 689,588 | 216,563 | 270,625 | 560,920 | -| collections | 113,511 | 116,816 | 21,138 | 22,045 | 77,912 | -| index signatures / 128 | 63,122 | 63,161 | 28,565 | 30,062 | 44,545 | -| index signatures / 512 | 16,412 | 16,338 | 5,432 | 4,593 | 6,029 | -| 200-row array payload | 23,071 | 23,094 | 5,624 | 4,809 | 15,663 | - -## Raw serializer adversarial cases - -These cases show where SchemaBinary loses. The shallow record makes fixed per-call costs visible. The clinical fixture is msgpackr's [`tests/example4.json`](https://github.com/kriszyp/msgpackr/blob/e3c852df383059b9ea8a8d3e5517d6e5527bf756/tests/example4.json), the input used by its own general benchmark. It has many nested, heterogeneous object shapes, which suit msgpackr's dynamic record cache. - -The clinical Schema is inferred once before timing. Objects at the same array path are merged, missing fields become optional, and mixed leaf types become unions. Schema inference and codec construction are excluded. The shared-structure Packr is primed once, matching msgpackr's steady-state benchmark setup. - -Raw / gzip -6 / zstd bytes: - -| Case | SchemaBinary | Fingerprint | Effect Msgpack schema | msgpackr shared | msgpackr plain | JSON raw | -| ------------------------- | -----------------: | -----------------: | --------------------: | -----------------: | -----------------: | -----------------: | -| shallow record | 47 / 70 / 56 | 30 / 50 / 39 | 69 / 88 / 78 | 24 / 42 / 33 | 69 / 88 / 78 | 89 / 100 / 92 | -| msgpackr clinical fixture | 4433 / 2282 / 2317 | 3513 / 1623 / 1672 | 6357 / 2364 / 2438 | 3821 / 1604 / 1681 | 6357 / 2364 / 2435 | 7569 / 2201 / 2288 | - -Average operations per second with msgpackr native acceleration enabled: - -| Case | Direction | SchemaBinary | Fingerprint | Effect Msgpack schema | msgpackr shared | msgpackr plain | JSON raw | -| ------------------------- | --------- | -----------: | ----------: | --------------------: | --------------: | -------------: | --------: | -| shallow record | encode | 1,704,527 | 1,842,472 | 798,150 | 2,463,086 | 2,806,880 | 2,928,575 | -| shallow record | decode | 2,039,262 | 2,202,957 | 891,250 | 6,906,994 | 4,324,485 | 2,280,053 | -| msgpackr clinical fixture | encode | 52,186 | 58,306 | 17,531 | 67,371 | 61,381 | 131,325 | -| msgpackr clinical fixture | decode | 48,462 | 58,404 | 16,928 | 183,899 | 53,859 | 56,427 | - -Clinical-fixture decode operations per second with native acceleration toggled: - -| Format | Enabled | Disabled | -| --------------------- | ------: | -------: | -| SchemaBinary | 48,462 | 48,476 | -| Fingerprint | 58,404 | 58,441 | -| Effect Msgpack schema | 16,928 | 15,124 | -| msgpackr shared | 183,899 | 118,799 | -| msgpackr plain | 53,859 | 40,537 | -| JSON raw | 56,427 | 57,388 | - -Shared-structure msgpackr still leads the clinical fixture: 1.29x on encode and 3.79x on decode against default SchemaBinary with native extraction enabled, and 2.45x on decode with it disabled. Fingerprint mode narrows that to 1.16x and 3.15x while staying 8% smaller than the shared-structure payload. - -Against everything that does not generate code, SchemaBinary is at or ahead of the field on the clinical fixture. Fingerprint decode beats plain msgpackr by 1.08x with native extraction enabled and 1.44x with it disabled, and beats raw `JSON.parse` by 1.03x. Fingerprint encode is within 5% of plain msgpackr, though `JSON.stringify` is 2.3x ahead of both. Against the schema-validating Effect Msgpack API, default SchemaBinary is 3.0x faster to encode and 2.9x faster to decode. - -The shallow record is where the remaining fixed cost shows. Both directions carry the parse pipeline around the codec, roughly a fifth of a shallow decode, which msgpackr does not pay because `unpack` is one function call. - -The rest of the shared-structure gap is code generation. msgpackr builds one reader per record structure with `new Function`, so a decoded object is an object literal: about 1 ns per property against 8 to 9 ns for the keyed store this codec has to use. That is worth roughly 2.3 us of the clinical fixture's 20 us decode. Removing the layout dispatch on top of it would leave around 13 us, still short of the 5.4 us shared-structure msgpackr reaches with its native string extractor. Closing that gap is a `new Function` decision, not a tuning one. - -## Streaming decode throughput - -Average decoded values per second for batched input: - -| Case | Default | Fingerprint | Msgpack | Protobuf | NDJSON | -| ---------------------- | --------: | ----------: | ------: | --------: | ------: | -| small record | 5,116,343 | 5,956,797 | 967,638 | 4,170,440 | 831,994 | -| nested payload | 821,034 | 1,042,459 | 284,246 | 586,806 | 312,906 | -| collections | 121,413 | 123,472 | 21,071 | 77,768 | 20,555 | -| index signatures / 128 | 65,080 | 65,325 | 28,163 | 38,953 | 28,743 | -| index signatures / 512 | 15,774 | 16,146 | 4,117 | 5,135 | 5,071 | -| 200-row array payload | 23,238 | 23,247 | 6,573 | 7,455 | 4,988 | -| 200 single-row frames | 2,257,102 | 2,682,446 | 849,319 | 1,692,708 | 950,570 | - -Average decoded values per second for single and first-byte-fragmented input: - -| Case | Default single | Default fragmented | Fingerprint single | Fingerprint fragmented | NDJSON single | NDJSON fragmented | -| ---------------------- | -------------: | -----------------: | -----------------: | ---------------------: | ------------: | ----------------: | -| small record | 3,020,534 | 2,168,195 | 3,695,255 | 2,720,058 | 131,636 | 133,208 | -| nested payload | 721,200 | 677,292 | 890,654 | 833,766 | 104,947 | 103,084 | -| collections | 119,996 | 118,256 | 121,800 | 119,050 | 18,668 | 18,652 | -| index signatures / 128 | 64,295 | 64,417 | 64,764 | 64,364 | 24,908 | 24,547 | -| index signatures / 512 | 16,424 | 16,396 | 16,581 | 16,488 | 5,195 | 5,098 | -| 200-row array payload | 23,468 | 23,205 | 23,418 | 23,052 | 5,251 | 5,218 | -| 200 single-row frames | 1,505,519 | 1,473,846 | 1,915,049 | 1,644,030 | 131,373 | 126,835 | - -## Analysis +The streaming section compares reusable SchemaBinary parsers, Protobuf delimited decoding, and NDJSON channels. It covers single frames, batches, and frames fragmented after the first byte. Codec, schema, and Protobuf descriptor construction are excluded from timings. -- Fingerprint mode is now the smallest SchemaBinary format for every case except the two index-signature maps, where the two modes are within 7 bytes: row runs apply in both modes, and fingerprint shapes are presence masks with no id list, so the `200-row array payload` dropped from 19,454 to 7,741 bytes. Its decode rate rose from 14,905 to 23,321 ops/s, matching the default mode. -- The default mode has the smallest raw payload of any non-fingerprint format in every case except the small record, where only Protobuf's one-byte field numbers beat its hashed five-byte field tags (42 vs 47 bytes). Fingerprint mode wins there too (30 bytes). -- Compression still changes the map ranking: Protobuf has the smallest zstd output for both index-signature cases, and JSON wins gzip at 128 keys. -- Protobuf keeps the small record in both directions, by about 1.5x. SchemaBinary leads the other five one-shot cases in both directions, from 1.05x on the nested payload up to 2.7x on index-signature decode, while returning schema-validated application values. Against Msgpack through the same public API it leads every case, by 1.9x to 6.4x. -- Single-frame and fragmented streaming rates for the `200 single-row frames` case carry 20% or worse RME at 250 samples, so only their batch column is worth comparing across runs. +Compare formats within the same case and run. Absolute throughput varies with the machine and runtime. diff --git a/repos/effect/packages/effect/benchmark/schema/SchemaBinary.ts b/repos/effect/packages/effect/benchmark/schema/SchemaBinary.ts index 1b1d931262..7077ec325d 100644 --- a/repos/effect/packages/effect/benchmark/schema/SchemaBinary.ts +++ b/repos/effect/packages/effect/benchmark/schema/SchemaBinary.ts @@ -1,9 +1,6 @@ import { Effect, Schema, Stream } from "effect" -import { Msgpack, Ndjson, SchemaBinary } from "effect/unstable/encoding" -import { isNativeAccelerationEnabled, pack, Packr, unpack, Unpackr } from "msgpackr" +import { Ndjson, SchemaBinary } from "effect/unstable/encoding" import assert from "node:assert/strict" -import { Buffer } from "node:buffer" -import { readFileSync } from "node:fs" import { gzipSync, zstdCompressSync } from "node:zlib" import protobuf, { type Message, type Type } from "protobufjs" import { Bench } from "tinybench" @@ -81,108 +78,6 @@ const largeRows = Array.from({ length: repeatedRecordStreamSize }, (_, index) => const metrics = (count: number) => Object.fromEntries(Array.from({ length: count }, (_, index) => [`metric-${index}`, index * 1.25])) -type InferredNode = - | { readonly _tag: "never" } - | { readonly _tag: "null" | "string" | "number" | "boolean" } - | { readonly _tag: "array"; readonly element: InferredNode } - | { - readonly _tag: "object" - readonly count: number - readonly fields: ReadonlyMap - } - | { readonly _tag: "union"; readonly members: ReadonlyMap } - -const inferredNever: InferredNode = { _tag: "never" } - -const inferredMembers = (node: InferredNode): ReadonlyMap => - node._tag === "union" ? node.members : new Map([[node._tag, node]]) - -const mergeInferred = (left: InferredNode, right: InferredNode): InferredNode => { - if (left._tag === "never") return right - if (right._tag === "never") return left - if (left._tag === right._tag) { - if (left._tag === "array" && right._tag === "array") { - return { _tag: "array", element: mergeInferred(left.element, right.element) } - } - if (left._tag === "object" && right._tag === "object") { - const fields = new Map(left.fields) - for (const [key, field] of right.fields) { - const previous = fields.get(key) - fields.set( - key, - previous === undefined - ? field - : { seen: previous.seen + field.seen, node: mergeInferred(previous.node, field.node) } - ) - } - return { _tag: "object", count: left.count + right.count, fields } - } - if (left._tag === "union" && right._tag === "union") { - const members = new Map(left.members) - for (const [tag, member] of right.members) { - members.set(tag, members.has(tag) ? mergeInferred(members.get(tag)!, member) : member) - } - return { _tag: "union", members } - } - return left - } - const members = new Map(inferredMembers(left)) - for (const [tag, member] of inferredMembers(right)) { - members.set(tag, members.has(tag) ? mergeInferred(members.get(tag)!, member) : member) - } - return { _tag: "union", members } -} - -const inferNode = (value: unknown): InferredNode => { - if (value === null) return { _tag: "null" } - if (typeof value === "string") return { _tag: "string" } - if (typeof value === "number") return { _tag: "number" } - if (typeof value === "boolean") return { _tag: "boolean" } - if (Array.isArray(value)) { - return { _tag: "array", element: value.reduce((node, item) => mergeInferred(node, inferNode(item)), inferredNever) } - } - if (typeof value === "object") { - return { - _tag: "object", - count: 1, - fields: new Map(Object.entries(value).map(([key, field]) => [key, { seen: 1, node: inferNode(field) }])) - } - } - throw new Error(`Cannot infer a benchmark schema for ${typeof value}`) -} - -const inferredSchema = (node: InferredNode): Schema.ConstraintCodec => { - switch (node._tag) { - case "never": - return Schema.Never - case "null": - return Schema.Null - case "string": - return Schema.String - case "number": - return Schema.Number - case "boolean": - return Schema.Boolean - case "array": - return Schema.Array(inferredSchema(node.element)) - case "union": - return Schema.Union(Array.from(node.members.values(), inferredSchema)) - case "object": - return Schema.Struct(Object.fromEntries(Array.from(node.fields, ([key, field]) => [ - key, - field.seen === node.count - ? inferredSchema(field.node) - : Schema.optionalKey(inferredSchema(field.node)) - ]))) - } -} - -// From msgpackr's benchmark corpus at e3c852d: https://github.com/kriszyp/msgpackr/blob/e3c852df383059b9ea8a8d3e5517d6e5527bf756/tests/example4.json -const msgpackrClinicalValue: unknown = JSON.parse( - readFileSync(new URL("./fixtures/msgpackr-example4.json", import.meta.url), "utf8") -) -const MsgpackrClinical = inferredSchema(inferNode(msgpackrClinicalValue)) - const protobufRoot = protobuf.parse(` syntax = "proto3"; @@ -405,19 +300,6 @@ const cases = [ } ] as const -const rawCases = [ - { - name: "small record / raw serializers", - schema: SmallRecord, - value: cases[0].value - }, - { - name: "msgpackr clinical fixture / raw serializers", - schema: MsgpackrClinical, - value: msgpackrClinicalValue - } -] as const - interface Format { readonly name: string readonly encodedSize: number @@ -470,7 +352,6 @@ const prepare = >( const binaryCodec = SchemaBinary.toCodec(schema) const fingerprintCodec = SchemaBinary.toCodec(schema, { fingerprint: true }) const jsonCodec = Schema.fromJsonString(jsonSchema) - const msgpackCodec = Msgpack.schema(jsonSchema) const binaryEncode = Schema.encodeUnknownSync(binaryCodec) const binaryDecode = Schema.decodeUnknownSync(binaryCodec) @@ -478,22 +359,18 @@ const prepare = >( const fingerprintDecode = Schema.decodeUnknownSync(fingerprintCodec) const jsonEncode = Schema.encodeUnknownSync(jsonCodec) const jsonDecode = Schema.decodeUnknownSync(jsonCodec) - const msgpackEncode = Schema.encodeUnknownSync(msgpackCodec) - const msgpackDecode = Schema.decodeUnknownSync(msgpackCodec) const protobufValue = protobufFixture.encodeInput(value) const binary = binaryEncode(value) const fingerprint = fingerprintEncode(value).slice() const json = jsonEncode(value) const jsonBytes = textEncoder.encode(json) - const msgpack = msgpackEncode(value) const protobufBytes = protobufFixture.type.encode(protobufValue).finish() const protobufDecode = () => protobufFixture.decodeOutput(protobufFixture.type.decode(protobufBytes)) assert.deepStrictEqual(binaryDecode(binary), value) assert.deepStrictEqual(fingerprintDecode(fingerprint), value) assert.deepStrictEqual(jsonDecode(json), value) - assert.deepStrictEqual(msgpackDecode(msgpack), value) assert.deepStrictEqual(protobufDecode(), value) return { @@ -516,12 +393,6 @@ const prepare = >( encode: () => jsonEncode(value), decode: () => jsonDecode(json) }, - { - name: "Msgpack", - ...sizes(msgpack), - encode: () => msgpackEncode(value), - decode: () => msgpackDecode(msgpack) - }, { name: "Protobuf", ...sizes(protobufBytes), @@ -532,88 +403,11 @@ const prepare = >( } } -const prepareRaw = >( - schema: S, - value: S["Type"] -): { readonly formats: ReadonlyArray } => { - const binaryCodec = SchemaBinary.toCodec(schema) - const fingerprintCodec = SchemaBinary.toCodec(schema, { fingerprint: true }) - const msgpackCodec = Msgpack.schema(Schema.toCodecJson(schema)) - const binaryEncode = Schema.encodeUnknownSync(binaryCodec) - const binaryDecode = Schema.decodeUnknownSync(binaryCodec) - const fingerprintEncode = Schema.encodeUnknownSync(fingerprintCodec) - const fingerprintDecode = Schema.decodeUnknownSync(fingerprintCodec) - const msgpackEncode = Schema.encodeUnknownSync(msgpackCodec) - const msgpackDecode = Schema.decodeUnknownSync(msgpackCodec) - const sharedPackr = new Packr({ structures: [] }) - - sharedPackr.pack(value) - const binary = binaryEncode(value).slice() - const fingerprint = fingerprintEncode(value).slice() - const effectMsgpack = msgpackEncode(value) - const sharedMsgpack = sharedPackr.pack(value).slice() - const plainMsgpack = pack(value).slice() - const json = Buffer.from(JSON.stringify(value)) - - assert.deepStrictEqual(binaryDecode(binary), value) - assert.deepStrictEqual(fingerprintDecode(fingerprint), value) - assert.deepStrictEqual(msgpackDecode(effectMsgpack), value) - assert.deepStrictEqual(sharedPackr.unpack(sharedMsgpack), value) - assert.deepStrictEqual(unpack(plainMsgpack), value) - assert.deepStrictEqual(JSON.parse(json.toString()), value) - - return { - formats: [ - { - name: "SchemaBinary", - ...sizes(binary), - encode: () => binaryEncode(value), - decode: () => binaryDecode(binary) - }, - { - name: "SchemaBinary fingerprint", - ...sizes(fingerprint), - encode: () => fingerprintEncode(value), - decode: () => fingerprintDecode(fingerprint) - }, - { - name: "Effect Msgpack schema", - ...sizes(effectMsgpack), - encode: () => msgpackEncode(value), - decode: () => msgpackDecode(effectMsgpack) - }, - { - name: "msgpackr raw / shared structures", - ...sizes(sharedMsgpack), - encode: () => sharedPackr.pack(value), - decode: () => sharedPackr.unpack(sharedMsgpack) - }, - { - name: "msgpackr raw / plain", - ...sizes(plainMsgpack), - encode: () => pack(value), - decode: () => unpack(plainMsgpack) - }, - { - name: "JSON raw", - ...sizes(json), - encode: () => Buffer.from(JSON.stringify(value)), - decode: () => JSON.parse(json.toString()) - } - ] - } -} - const prepared = cases.map((testCase) => ({ name: testCase.name, ...prepare(testCase.schema, testCase.value, testCase.protobuf) })) -const preparedRaw = rawCases.map((testCase) => ({ - name: testCase.name, - ...prepareRaw(testCase.schema, testCase.value) -})) - const prepareStream = async >( schema: S, values: ReadonlyArray, @@ -635,11 +429,7 @@ const prepareStream = async > }) const jsonSchema = Schema.toCodecJson(schema) - const encodeMsgpackValue = Schema.encodeUnknownSync(jsonSchema) - const decodeMsgpackValue = Schema.decodeUnknownSync(jsonSchema) - const msgpackPackr = new Packr() - const msgpackStream = concatFrames(values.map((value) => msgpackPackr.pack(encodeMsgpackValue(value)).slice())) - const msgpackUnpackr = new Unpackr() + const encodeJsonValue = Schema.encodeUnknownSync(jsonSchema) const protobufFrames = values.map((value) => protobufFixture.type.encodeDelimited(protobufFixture.encodeInput(value)).finish() @@ -654,7 +444,7 @@ const prepareStream = async > return decoded } - const ndjsonFrames = values.map((value) => textEncoder.encode(`${JSON.stringify(encodeMsgpackValue(value))}\n`)) + const ndjsonFrames = values.map((value) => textEncoder.encode(`${JSON.stringify(encodeJsonValue(value))}\n`)) const ndjsonStream = concatFrames(ndjsonFrames) const ndjsonFragments = ndjsonFrames.map((frame) => { return [frame.subarray(0, 1), frame.subarray(1)] as const @@ -700,16 +490,12 @@ const prepareStream = async > const decodeSingle = defaultFeeds.single const decodeFragmented = defaultFeeds.fragmented const decodeBatch = defaultFeeds.batch - const decodeMsgpackStream = () => - msgpackUnpackr.unpackMultiple(msgpackStream).map((value) => decodeMsgpackValue(value)) - assert.deepStrictEqual(decodeSingle(), [values[0]]) assert.deepStrictEqual(decodeFragmented(), [values[0]]) assert.deepStrictEqual(decodeBatch(), values) assert.deepStrictEqual(fingerprintFeeds.single(), [values[0]]) assert.deepStrictEqual(fingerprintFeeds.fragmented(), [values[0]]) assert.deepStrictEqual(fingerprintFeeds.batch(), values) - assert.deepStrictEqual(decodeMsgpackStream(), values) assert.deepStrictEqual(decodeProtobufStream(), values) assert.deepStrictEqual(await decodeNdjsonSingle(), [values[0]]) assert.deepStrictEqual(await decodeNdjsonFragmented(), [values[0]]) @@ -723,7 +509,6 @@ const prepareStream = async > { name: "SchemaBinary fingerprint / single frame", framesPerOp: 1, decode: fingerprintFeeds.single }, { name: "SchemaBinary fingerprint / batch", framesPerOp: values.length, decode: fingerprintFeeds.batch }, { name: "SchemaBinary fingerprint / fragmented", framesPerOp: 1, decode: fingerprintFeeds.fragmented }, - { name: "Msgpack unpackMultiple / batch", framesPerOp: values.length, decode: decodeMsgpackStream }, { name: "Protobuf decodeDelimited / batch", framesPerOp: values.length, decode: decodeProtobufStream }, { name: "NDJSON Channel / single frame", framesPerOp: 1, decode: decodeNdjsonSingle }, { name: "NDJSON Channel / batch", framesPerOp: values.length, decode: decodeNdjsonBatch }, @@ -732,7 +517,6 @@ const prepareStream = async > sizes: [ { name: "SchemaBinary", frames: values.length, ...sizes(binaryStream) }, { name: "SchemaBinary fingerprint", frames: values.length, ...sizes(fingerprintStream) }, - { name: "Msgpack", frames: values.length, ...sizes(msgpackStream) }, { name: "Protobuf", frames: values.length, ...sizes(protobufStream) }, { name: "NDJSON", frames: values.length, ...sizes(ndjsonStream) } ] @@ -752,11 +536,7 @@ const preparedStreams = await Promise.all([ ].map(async ({ name, prepared }) => ({ name, ...await prepared }))) console.log(`Node ${process.version}; codec and schema construction excluded from timings.`) -console.log("JSON and Msgpack use the same Schema.toCodecJson representation; JSON sizes are UTF-8 bytes.") -console.log(`msgpackr native acceleration enabled: ${isNativeAccelerationEnabled}.`) -console.log( - "Cases suffixed with / raw serializers compare SchemaBinary's public codec with unvalidated raw serializers." -) +console.log("JSON sizes are UTF-8 bytes.") console.log("Protobuf uses prebuilt protobufjs descriptors; descriptor construction and encode adapters are excluded.") console.log( "Compare formats within a case and direction in the same run; absolute rates vary with the machine and runtime." @@ -772,16 +552,6 @@ console.table(prepared.flatMap((testCase) => })) )) -console.table(preparedRaw.flatMap((testCase) => - testCase.formats.map((format) => ({ - Case: testCase.name, - Format: format.name, - "Raw bytes": format.encodedSize, - "gzip -6 bytes": format.gzipSize, - "zstd bytes": format.zstdSize - })) -)) - console.log( "SchemaBinary streaming reuses one parser per feed shape; NDJSON runs its Channel per operation. Fragmented frames split after the first byte." ) @@ -820,18 +590,6 @@ for (const testCase of prepared) { } } -for (const testCase of preparedRaw) { - for (const format of testCase.formats) { - for (const [direction, run] of [["encode", format.encode], ["decode", format.decode]] as const) { - const name = `${testCase.name} / ${format.name} / ${direction}` - tasks.set(name, { caseName: testCase.name, formatName: format.name, direction }) - bench.add(name, () => { - sink = run() - }) - } - } -} - await bench.run() if (sink === sinkSentinel) { diff --git a/repos/effect/packages/effect/benchmark/schema/fixtures/msgpackr-example4.json b/repos/effect/packages/effect/benchmark/schema/fixtures/msgpackr-example4.json deleted file mode 100644 index 585c88e55d..0000000000 --- a/repos/effect/packages/effect/benchmark/schema/fixtures/msgpackr-example4.json +++ /dev/null @@ -1,284 +0,0 @@ -{ - "metadata": { - "Designs": ["Randomized Controlled Trial"], - "Types": [], - "BriefSummary": "To determine the efficacy, long-term safety, and tolerability of alirocumab 300 mg every 4\n weeks (Q4W), in comparison with placebo, as well as its potential as a starting regimen. The\n dose regimen of 75 mg every 2 weeks (Q2W), as used in other studies, was added as a\n calibrator.", - "Abstract": "To determine the efficacy, long-term safety, and tolerability of alirocumab 300 mg every 4\n weeks (Q4W), in comparison with placebo, as well as its potential as a starting regimen. The\n dose regimen of 75 mg every 2 weeks (Q2W), as used in other studies, was added as a\n calibrator.", - "Acronym": null, - "ArticleId": "Qy3gwKWSoaWRmbmFEQA", - "Authors": null, - "CochraneID": null, - "Confidential": false, - "CorporateAuthor": null, - "Country": "Bulgaria, Canada, Hungary, Israel, Norway, Slovakia, United Kingdom, United States", - "CustomData": null, - "DatabaseType": "ClinicalTrials.gov", - "DOI": null, - "EmbaseAccessionNumber": null, - "Emtree": null, - "ErrataText": null, - "FullTextURL": null, - "Institution": null, - "ISSN": null, - "Issue": null, - "JournalTitle": null, - "MedlineID": null, - "MeSH": "Hypercholesterolemia|Antibodies, Monoclonal", - "Pages": null, - "ParentChildStatus": null, - "ParentID": null, - "PublicationDate": "March 21, 2017", - "PublicationYear": 2017, - "PubType": null, - "ReferenceStudy": null, - "SecondarySourceID": null, - "Source": "Regeneron Pharmaceuticals", - "SourceReferenceId": "NCT01926782", - "TaStudyDesign": "Randomized", - "Title": "A Randomized, Double-Blind, Placebo-Controlled Study to Evaluate the Efficacy and Safety of an Every Four Weeks Treatment Regimen of Alirocumab in Patients With Primary Hypercholesterolemia", - "TrialOutcome": null, - "Volume": null, - "Id": 179246831, - "Created": "2020-04-10T14:48:20.4384957Z", - "VersionNo": 2, - "ExtractData": null, - "Digitized": true, - "IsRapidExtract": false, - "IsUploaded": false - }, - "design": "Randomized Controlled Trial", - "conditions": [{ "label": "Cholesterol Total Increased", "id": "SUE_c" }], - "phase": 3, - "name": "NCT01926782", - "trialIds": ["NCT01926782"], - "acronyms": [], - "outcomeCount": 156, - "id": 179246831, - "groups": [ - { - "Id": "4r", - "RefId": "B5|O2~Alirocumab 75 mg Q2W/Up 150 mg Q2W Without Concomitant Statin", - "OriginalName": "Alirocumab 75 mg Q2W/Up 150 mg Q2W Without Concomitant Statin", - "N": 37, - "age": 59.3, - "ageSD": 11.3, - "male": 37.83783783783784, - "Interventions": [{ "termIds": [["SUBYEL", "SUB_Oc"], ["SUNUVb"]] }], - "analyzeAs": "Alirocumab", - "analyzableScore": 1.0717734625362931, - "matchingScore": 0 - }, - { - "Id": "zB", - "RefId": "B6|O3~Alirocumab 300 mg Q4W/Up 150 mg Q2W Without Concomitant Statin", - "OriginalName": "Alirocumab 300 mg Q4W/Up 150 mg Q2W Without Concomitant Statin", - "N": 146, - "age": 59.2, - "ageSD": 10.8, - "male": 45.205479452054796, - "Interventions": [{ "termIds": [["SUBYEL", "SUB_Oc"]] }], - "analyzeAs": "Statins", - "analyzableScore": 1.0717734625362931, - "matchingScore": 0 - }, - { - "Id": "3!", - "RefId": "B4|O1~Placebo Q2W Without Concomitant Statin", - "OriginalName": "Placebo Q2W Without Concomitant Statin", - "N": 73, - "age": 59.4, - "ageSD": 10.2, - "male": 54.794520547945204, - "Interventions": [{ "termIds": [["SUGeLS"], ["SUBYEL", "SUB_Oc"]] }], - "analyzeAs": "Control", - "analyzableScore": 1.2020833333333334, - "matchingScore": 0 - }, - { - "Id": "tv", - "RefId": "E3", - "OriginalName": "Alirocumab 300 mg Q4W/Up 150 mg Q2W", - "Interventions": [{ "termIds": [["SUCO54", "SUNUVb"]] }] - }, - { - "Id": "jt", - "RefId": "B3|O3~Alirocumab 300 mg Q4W/Up 150 mg Q2W With Concomitant Statin", - "OriginalName": "Alirocumab 300 mg Q4W/Up 150 mg Q2W With Concomitant Statin", - "N": 312, - "age": 61.6, - "ageSD": 10, - "male": 60.8974358974359, - "Interventions": [{ "termIds": [["SUBYEL", "SUB_Oc"]] }] - }, - { - "Id": "5!", - "RefId": "E2", - "OriginalName": "Alirocumab 75 mg Q2W/Up 150 mg Q2W", - "Interventions": [{ "termIds": [["SUNUVb"]] }] - }, - { - "Id": "4E", - "RefId": "B2|O2~Alirocumab 75 mg Q2W/Up 150 mg Q2W With Concomitant Statin", - "OriginalName": "Alirocumab 75 mg Q2W/Up 150 mg Q2W With Concomitant Statin", - "N": 78, - "age": 60.7, - "ageSD": 9.1, - "male": 65.38461538461539, - "Interventions": [{ "termIds": [["SUBYEL", "SUB_Oc"], ["SUNUVb"]] }] - }, - { - "Id": "i4", - "Interventions": [ - { - "Id": "Ya", - "Name": 178613599, - "Treatments": [{ "Id": "((", "Phase": "k)" }], - "Type": "Drug", - "termIds": [["SUGeLS"], ["SUNUVb"]], - "terms": [["Placebo"], ["Alirocumab"]] - }, - { - "Id": "o)", - "Name": 2159990, - "Treatments": [{ "Id": "1$", "Phase": "k)" }], - "Type": "Drug", - "termIds": [["SUBYEL"]], - "terms": [["Statins"]] - } - ], - "RefId": "E1|Placebo Q2W", - "OriginalName": "Placebo Q2W" - }, - { - "Id": "Ls", - "RefId": "B1|O1~Placebo Q2W With Concomitant Statin", - "OriginalName": "Placebo Q2W With Concomitant Statin", - "N": 157, - "age": 61.6, - "ageSD": 9.7, - "male": 64.3312101910828, - "Interventions": [{ "termIds": [["SUGeLS"], ["SUBYEL", "SUB_Oc"]] }] - } - ], - "hasDocData": true, - "hasRapidExtract": false, - "N": 803, - "queryScore": 1.4868329805051381, - "matchingScore": 7.960635921410255, - "score": 22.084654254966498, - "outcomes": [ - { - "id": "179246387", - "type": "Change", - "unit": "%", - "termIds": [["SUF0R", "SUBskP"]], - "quantifiers": [], - "name": "Calculated LDL-C in Not Receiving Concomitant Statin Therapy - On-Treatment Analysis", - "cells": [ - { "number": -0.4, "unit": "%", "group": "3!", "varType": "se", "N": 70, "se": 2, "sd": 16.73 }, - { "number": -54.6, "unit": "%", "group": "4r", "varType": "se", "N": 37, "se": 2.8, "sd": 17.03 }, - { "number": -59.4, "unit": "%", "group": "zB", "varType": "se", "N": 141, "se": 1.4, "sd": 16.62 } - ], - "time": { - "Id": 67122072, - "Low": { "Value": "Baseline" }, - "High": { "Number": 24, "Unit": "wk" }, - "Type": "Total", - "days": 168, - "description": "24wk" - }, - "score": 2.08, - "matchingTerm": "SUF0R", - "suggestedPositive": false, - "sourceUnit": "%" - }, - { - "id": "179246389", - "type": "Change", - "unit": "%", - "termIds": [["SUF0R", "SUBskP"]], - "quantifiers": [], - "name": "Calculated LDL-C in Receiving Concomitant Statin Therapy - On-Treatment Analysis", - "cells": [ - { "number": -0.3, "unit": "%", "group": "Ls", "varType": "se", "N": 151, "se": 2.1, "sd": 25.81 }, - { "number": -55.1, "unit": "%", "group": "4E", "varType": "se", "N": 75, "se": 3, "sd": 25.98 }, - { "number": -62.3, "unit": "%", "group": "jt", "varType": "se", "N": 302, "se": 1.5, "sd": 26.07 } - ], - "time": { - "Id": 67122072, - "Low": { "Value": "Baseline" }, - "High": { "Number": 24, "Unit": "wk" }, - "Type": "Total", - "days": 168, - "description": "24wk" - }, - "score": 2.08, - "matchingTerm": "SUF0R", - "suggestedPositive": false, - "sourceUnit": "%" - }, - { - "id": "179246393", - "type": "Change", - "unit": "%", - "termIds": [["SUF0R", "SUBskP"]], - "quantifiers": [], - "name": "Calculated LDL-C in Not Receiving Concomitant Statin Therapy - On-Treatment Analysis", - "cells": [ - { "number": -0.5, "unit": "%", "group": "3!", "varType": "se", "N": 70, "se": 2, "sd": 16.73 }, - { "number": -53.9, "unit": "%", "group": "4r", "varType": "se", "N": 37, "se": 2.7, "sd": 16.42 }, - { "number": -60, "unit": "%", "group": "zB", "varType": "se", "N": 141, "se": 1.4, "sd": 16.62 } - ], - "time": { - "Id": 67122069, - "Low": { "Value": "Baseline" }, - "High": { "Number": 12, "Unit": "wk" }, - "Type": "Total", - "days": 84, - "description": "12wk" - }, - "score": 2.08, - "matchingTerm": "SUF0R", - "suggestedPositive": false, - "sourceUnit": "%" - }, - { - "id": "179246394", - "type": "Change", - "unit": "%", - "termIds": [["SUF0R", "SUBskP"]], - "quantifiers": [], - "name": "Calculated LDL-C in Receiving Concomitant Statin Therapy - On-Treatment Analysis", - "cells": [ - { "number": 1.4, "unit": "%", "group": "Ls", "varType": "se", "N": 151, "se": 1.9, "sd": 23.35 }, - { "number": -47.3, "unit": "%", "group": "4E", "varType": "se", "N": 75, "se": 2.8, "sd": 24.25 }, - { "number": -58, "unit": "%", "group": "jt", "varType": "se", "N": 302, "se": 1.4, "sd": 24.33 } - ], - "time": { - "Id": 67122069, - "Low": { "Value": "Baseline" }, - "High": { "Number": 12, "Unit": "wk" }, - "Type": "Total", - "days": 84, - "description": "12wk" - }, - "score": 2.08, - "matchingTerm": "SUF0R", - "suggestedPositive": false, - "sourceUnit": "%" - } - ], - "characteristics": [ - { - "id": "179246354", - "type": "Binary", - "isCharacteristic": true, - "termIds": [["SUE_c", "SUCbN", "SUyJj"]], - "quantifiers": [], - "name": "Patients not having adequate control of their hypercholesterolemia based on their individual level of CVD risk", - "cells": [], - "number": 100 - } - ], - "outcomesScore": 18.97947630112307 -} diff --git a/repos/effect/packages/effect/package.json b/repos/effect/packages/effect/package.json index 21bcb291ed..ab94757092 100644 --- a/repos/effect/packages/effect/package.json +++ b/repos/effect/packages/effect/package.json @@ -31,6 +31,7 @@ ".": "./src/index.ts", "./testing": "./src/testing/index.ts", "./unstable/ai": "./src/unstable/ai/index.ts", + "./unstable/arbitrary": "./src/unstable/arbitrary/index.ts", "./unstable/cli": "./src/unstable/cli/index.ts", "./unstable/cluster": "./src/unstable/cluster/index.ts", "./unstable/devtools": "./src/unstable/devtools/index.ts", @@ -38,6 +39,7 @@ "./unstable/eventlog": "./src/unstable/eventlog/index.ts", "./unstable/http": "./src/unstable/http/index.ts", "./unstable/httpapi": "./src/unstable/httpapi/index.ts", + "./unstable/net": "./src/unstable/net/index.ts", "./unstable/observability": "./src/unstable/observability/index.ts", "./unstable/persistence": "./src/unstable/persistence/index.ts", "./unstable/process": "./src/unstable/process/index.ts", @@ -73,6 +75,7 @@ ".": "./dist/index.js", "./testing": "./dist/testing/index.js", "./unstable/ai": "./dist/unstable/ai/index.js", + "./unstable/arbitrary": "./dist/unstable/arbitrary/index.js", "./unstable/cli": "./dist/unstable/cli/index.js", "./unstable/cluster": "./dist/unstable/cluster/index.js", "./unstable/devtools": "./dist/unstable/devtools/index.js", @@ -80,6 +83,7 @@ "./unstable/eventlog": "./dist/unstable/eventlog/index.js", "./unstable/http": "./dist/unstable/http/index.js", "./unstable/httpapi": "./dist/unstable/httpapi/index.js", + "./unstable/net": "./dist/unstable/net/index.js", "./unstable/observability": "./dist/unstable/observability/index.js", "./unstable/persistence": "./dist/unstable/persistence/index.js", "./unstable/process": "./dist/unstable/process/index.js", @@ -105,17 +109,13 @@ "check": "tsc -b tsconfig.json" }, "devDependencies": { - "@types/node": "^26.2.0", + "@types/node": "^26.4.1", "ajv": "^8.20.0", "ajv-draft-04": "^1.0.0", - "ast-types": "^0.14.2", + "ast-types": "^0.16.3", "immer": "^11.1.18", - "protobufjs": "^7.6.5", - "tinybench": "^6.1.3", + "protobufjs": "^8.8.0", + "tinybench": "^6.1.6", "valibot": "^1.4.2" - }, - "dependencies": { - "fast-check": "^4.9.0", - "msgpackr": "^2.0.5" } } diff --git a/repos/effect/packages/effect/runtimeperf/README.md b/repos/effect/packages/effect/runtimeperf/README.md index 8ff0458bbd..8265911a73 100644 --- a/repos/effect/packages/effect/runtimeperf/README.md +++ b/repos/effect/packages/effect/runtimeperf/README.md @@ -4,6 +4,7 @@ This harness measures focused synchronous runtime paths in fresh Node processes. It supports: - focused Effect Schema diagnostics; +- native Arbitrary comparisons against equivalent fast-check v4 arbitraries; - the upstream Effect, Valibot and Zod benchmark matrix; - paired comparisons between Git revisions or the current working tree. @@ -36,6 +37,7 @@ Select a suite, fixture, shared scenario, tier, family or implementation: ```sh pnpm runtimeperf schema +pnpm runtimeperf arbitrary pnpm runtimeperf object-32-valid pnpm runtimeperf schema/object-32-valid-effect pnpm runtimeperf --family arrays @@ -84,6 +86,34 @@ adapters, recursion and cold paths. The `schema-benchmarks` suite contains the complete timing matrices exposed by the upstream Effect, Valibot and Zod adapters. +The `arbitrary` suite compares the native public API with direct fast-check v4 arbitraries in separate processes. It +measures derivation through the first recursive sample, steady-state recursive +sampling, optional-Struct sampling, fixed-length string generation to exercise constraint pushdown, +bounded Number generation, direct Uint8Array generation, a rare residual filter, a fixed-length unique array, and a +mixed regular expression through cold derivation, warm generation, and shrinking. It also covers literal sampling as +a runner baseline and the public `map`, `filter`, `filterMap`, and `all` combinators for tuples and records. The failure +paths include a filtered failure with rejected shrink candidates, dependent `flatMap` sampling, shrinking, replay, a +passing property, a failure that shrinks from `1000` to `1`, and replay of that failure. The +recursive distributions are implementation-defined, so the fixtures use +implementation-specific size settings and validate a comparable total node +count for the fixed seed. The bounded Number case is a throughput comparison, +not distribution parity: native selects among 64-bit IEEE-754 representations, +while the fast-check fixture uses its 64-bit `double`. Replay is an +end-to-end public API comparison, but the work is not identical: native replay +verifies the original failure and its full shrink path, while fast-check can +start directly from its recorded path. + +The regular-expression fixtures validate the same language, and both fixed-seed warm fixtures must reach multiple +lengths and every alternative family. Their exact distributions remain implementation-defined, so cross-library +timings are diagnostic while base/head comparisons protect the cost of each implementation's established behavior. + +An impossible Schema filter is deliberately not a cross-engine timing case. +The native runner reports bounded exhaustion, while a direct fast-check arbitrary +built with `Arbitrary.filter` does not return from generation +when no value can satisfy the predicate. Native exhaustion remains covered by +the Arbitrary tests instead of placing a permanently blocking fixture in the +performance harness. + Zod parsing cases import `zod/v4` and call `safeParse` with `{ jitless: true }`; its Standard Schema and codec cases use their native APIs. Valibot uses the corresponding `is`, `safeParse` and Standard Schema APIs. The focused Effect diff --git a/repos/effect/packages/effect/runtimeperf/config.json b/repos/effect/packages/effect/runtimeperf/config.json index 92d07ddf0f..261711dcdb 100644 --- a/repos/effect/packages/effect/runtimeperf/config.json +++ b/repos/effect/packages/effect/runtimeperf/config.json @@ -11,6 +11,600 @@ "maxRegressionPercent": 5 }, "suites": [ + { + "name": "arbitrary", + "fixtures": [ + { + "file": "suites/arbitrary/fixtures/native.ts", + "defaults": { + "tier": 1, + "implementation": "effect", + "family": "arbitrary", + "astTags": [ + "Objects", + "Arrays", + "String", + "Number", + "Declaration", + "Suspend" + ], + "path": "valid" + }, + "cases": [ + { + "name": "cold-recursive-first-sample-native", + "export": "coldRecursiveFirstSample", + "scenario": "arbitrary-cold-recursive-first-sample", + "operation": "derive-and-sample", + "path": "cold", + "size": 1 + }, + { + "name": "recursive-sample-32-native", + "export": "recursiveSample32", + "scenario": "arbitrary-recursive-sample-32", + "operation": "sample", + "size": 32 + }, + { + "name": "optional-struct-sample-128-native", + "export": "optionalStructSample128", + "scenario": "arbitrary-optional-struct-sample-128", + "operation": "sample-optional-struct", + "astTags": [ + "Objects", + "Number" + ], + "size": 128 + }, + { + "name": "constrained-string-sample-128-native", + "export": "constrainedStringSample128", + "scenario": "arbitrary-constrained-string-sample-128", + "operation": "sample", + "astTags": [ + "String" + ], + "size": 128 + }, + { + "name": "cold-regexp-first-sample-native", + "export": "coldRegExpFirstSample", + "scenario": "arbitrary-cold-regexp-first-sample", + "operation": "derive-and-sample-pattern", + "path": "cold", + "astTags": [ + "String" + ], + "size": 1 + }, + { + "name": "regexp-sample-64-native", + "export": "regExpSample64", + "scenario": "arbitrary-regexp-sample-64", + "operation": "sample-pattern", + "astTags": [ + "String" + ], + "size": 64 + }, + { + "name": "regexp-check-falsify-shrink-native", + "export": "regExpCheckFalsifyAndShrink", + "scenario": "arbitrary-regexp-check-falsify-shrink", + "operation": "check-pattern-and-shrink", + "path": "failure", + "astTags": [ + "String" + ], + "size": 1 + }, + { + "name": "bounded-number-sample-128-native", + "export": "boundedNumberSample128", + "scenario": "arbitrary-bounded-number-sample-128", + "operation": "sample", + "astTags": [ + "Number" + ], + "size": 128 + }, + { + "name": "uint8-array-sample-128-native", + "export": "uint8ArraySample128", + "scenario": "arbitrary-uint8-array-sample-128", + "operation": "sample", + "astTags": [ + "Declaration", + "Arrays", + "Number" + ], + "size": 128 + }, + { + "name": "big-decimal-sample-128-native", + "export": "bigDecimalSample128", + "scenario": "arbitrary-big-decimal-sample-128", + "operation": "sample", + "astTags": [ + "Declaration", + "Objects", + "BigInt", + "Number" + ], + "size": 128 + }, + { + "name": "date-time-utc-sample-128-native", + "export": "dateTimeUtcSample128", + "scenario": "arbitrary-date-time-utc-sample-128", + "operation": "sample", + "astTags": [ + "Declaration", + "Number" + ], + "size": 128 + }, + { + "name": "time-zone-named-sample-128-native", + "export": "timeZoneNamedSample128", + "scenario": "arbitrary-time-zone-named-sample-128", + "operation": "sample", + "astTags": [ + "Declaration", + "Literal" + ], + "size": 128 + }, + { + "name": "time-zone-sample-128-native", + "export": "timeZoneSample128", + "scenario": "arbitrary-time-zone-sample-128", + "operation": "sample", + "astTags": [ + "Declaration", + "Union", + "Number", + "Literal" + ], + "size": 128 + }, + { + "name": "date-time-zoned-sample-128-native", + "export": "dateTimeZonedSample128", + "scenario": "arbitrary-date-time-zoned-sample-128", + "operation": "sample", + "astTags": [ + "Declaration", + "Objects", + "Union", + "Number", + "Literal" + ], + "size": 128 + }, + { + "name": "rare-filter-sample-32-native", + "export": "rareFilterSample32", + "scenario": "arbitrary-rare-filter-sample-32", + "operation": "sample", + "astTags": [ + "Number" + ], + "size": 32 + }, + { + "name": "unique-array-sample-32-native", + "export": "uniqueArraySample32", + "scenario": "arbitrary-unique-array-sample-32", + "operation": "sample", + "astTags": [ + "Arrays", + "Number" + ], + "size": 32 + }, + { + "name": "literal-sample-128-native", + "export": "literalSample128", + "scenario": "arbitrary-literal-sample-128", + "operation": "sample", + "astTags": [ + "Literal" + ], + "size": 128 + }, + { + "name": "map-sample-128-native", + "export": "mapSample128", + "scenario": "arbitrary-map-sample-128", + "operation": "sample-map", + "astTags": [ + "Number" + ], + "size": 128 + }, + { + "name": "passing-filter-sample-128-native", + "export": "passingFilterSample128", + "scenario": "arbitrary-passing-filter-sample-128", + "operation": "sample-filter", + "astTags": [ + "Number" + ], + "size": 128 + }, + { + "name": "selective-filter-sample-32-native", + "export": "selectiveFilterSample32", + "scenario": "arbitrary-selective-filter-sample-32", + "operation": "sample-filter", + "astTags": [ + "Number" + ], + "size": 32 + }, + { + "name": "filter-map-sample-128-native", + "export": "filterMapSample128", + "scenario": "arbitrary-filter-map-sample-128", + "operation": "sample-filter-map", + "astTags": [ + "Number" + ], + "size": 128 + }, + { + "name": "filter-check-falsify-shrink-native", + "export": "filterCheckFalsifyAndShrink", + "scenario": "arbitrary-filter-check-falsify-shrink", + "operation": "check-filter-and-shrink", + "path": "failure", + "astTags": [ + "Number" + ], + "size": 1 + }, + { + "name": "all-tuple-sample-128-native", + "export": "allTupleSample128", + "scenario": "arbitrary-all-tuple-sample-128", + "operation": "sample-all-tuple", + "astTags": [ + "Literal" + ], + "size": 128 + }, + { + "name": "all-record-sample-128-native", + "export": "allRecordSample128", + "scenario": "arbitrary-all-record-sample-128", + "operation": "sample-all-record", + "astTags": [ + "Union", + "Literal", + "Number" + ], + "size": 128 + }, + { + "name": "flat-map-sample-128-native", + "export": "flatMapSample128", + "scenario": "arbitrary-flat-map-sample-128", + "operation": "sample-flat-map", + "astTags": [ + "Arrays", + "Number" + ], + "size": 128 + }, + { + "name": "flat-map-check-falsify-shrink-native", + "export": "flatMapCheckFalsifyAndShrink", + "scenario": "arbitrary-flat-map-check-falsify-shrink", + "operation": "check-flat-map-and-shrink", + "path": "failure", + "astTags": [ + "Arrays", + "Number" + ], + "size": 1 + }, + { + "name": "flat-map-check-replay-native", + "export": "flatMapCheckReplay", + "scenario": "arbitrary-flat-map-check-replay", + "operation": "replay-flat-map", + "path": "failure", + "astTags": [ + "Arrays", + "Number" + ], + "size": 1 + }, + { + "name": "check-pass-100-native", + "export": "checkPass100", + "scenario": "arbitrary-check-pass-100", + "operation": "check", + "astTags": [ + "Number" + ], + "size": 100 + }, + { + "name": "test-schema-verify-generation-100-native", + "export": "testSchemaVerifyGeneration100", + "scenario": "test-schema-verify-generation-100", + "operation": "derive-and-check", + "astTags": [ + "Number" + ], + "size": 100 + }, + { + "name": "check-falsify-shrink-native", + "export": "checkFalsifyAndShrink", + "scenario": "arbitrary-check-falsify-shrink", + "operation": "check-and-shrink", + "path": "failure", + "astTags": [ + "Number" + ], + "size": 1 + }, + { + "name": "check-replay-native", + "export": "checkReplay", + "scenario": "arbitrary-check-replay", + "operation": "replay", + "path": "failure", + "astTags": [ + "Number" + ], + "size": 1 + } + ] + }, + { + "file": "suites/arbitrary/fixtures/fast-check-v4.ts", + "defaults": { + "tier": 1, + "implementation": "fast-check-v4", + "family": "arbitrary", + "astTags": [], + "path": "valid" + }, + "cases": [ + { + "name": "cold-recursive-first-sample-fast-check-v4", + "export": "coldRecursiveFirstSample", + "scenario": "arbitrary-cold-recursive-first-sample", + "operation": "derive-and-sample", + "path": "cold", + "size": 1 + }, + { + "name": "recursive-sample-32-fast-check-v4", + "export": "recursiveSample32", + "scenario": "arbitrary-recursive-sample-32", + "operation": "sample", + "size": 32 + }, + { + "name": "optional-struct-sample-128-fast-check-v4", + "export": "optionalStructSample128", + "scenario": "arbitrary-optional-struct-sample-128", + "operation": "sample-optional-struct", + "size": 128 + }, + { + "name": "constrained-string-sample-128-fast-check-v4", + "export": "constrainedStringSample128", + "scenario": "arbitrary-constrained-string-sample-128", + "operation": "sample", + "size": 128 + }, + { + "name": "cold-regexp-first-sample-fast-check-v4", + "export": "coldRegExpFirstSample", + "scenario": "arbitrary-cold-regexp-first-sample", + "operation": "derive-and-sample-pattern", + "path": "cold", + "size": 1 + }, + { + "name": "regexp-sample-64-fast-check-v4", + "export": "regExpSample64", + "scenario": "arbitrary-regexp-sample-64", + "operation": "sample-pattern", + "size": 64 + }, + { + "name": "regexp-check-falsify-shrink-fast-check-v4", + "export": "regExpCheckFalsifyAndShrink", + "scenario": "arbitrary-regexp-check-falsify-shrink", + "operation": "check-pattern-and-shrink", + "path": "failure", + "size": 1 + }, + { + "name": "bounded-number-sample-128-fast-check-v4", + "export": "boundedNumberSample128", + "scenario": "arbitrary-bounded-number-sample-128", + "operation": "sample", + "size": 128 + }, + { + "name": "uint8-array-sample-128-fast-check-v4", + "export": "uint8ArraySample128", + "scenario": "arbitrary-uint8-array-sample-128", + "operation": "sample", + "size": 128 + }, + { + "name": "big-decimal-sample-128-fast-check-v4", + "export": "bigDecimalSample128", + "scenario": "arbitrary-big-decimal-sample-128", + "operation": "sample", + "size": 128 + }, + { + "name": "date-time-utc-sample-128-fast-check-v4", + "export": "dateTimeUtcSample128", + "scenario": "arbitrary-date-time-utc-sample-128", + "operation": "sample", + "size": 128 + }, + { + "name": "time-zone-named-sample-128-fast-check-v4", + "export": "timeZoneNamedSample128", + "scenario": "arbitrary-time-zone-named-sample-128", + "operation": "sample", + "size": 128 + }, + { + "name": "time-zone-sample-128-fast-check-v4", + "export": "timeZoneSample128", + "scenario": "arbitrary-time-zone-sample-128", + "operation": "sample", + "size": 128 + }, + { + "name": "date-time-zoned-sample-128-fast-check-v4", + "export": "dateTimeZonedSample128", + "scenario": "arbitrary-date-time-zoned-sample-128", + "operation": "sample", + "size": 128 + }, + { + "name": "rare-filter-sample-32-fast-check-v4", + "export": "rareFilterSample32", + "scenario": "arbitrary-rare-filter-sample-32", + "operation": "sample", + "size": 32 + }, + { + "name": "unique-array-sample-32-fast-check-v4", + "export": "uniqueArraySample32", + "scenario": "arbitrary-unique-array-sample-32", + "operation": "sample", + "size": 32 + }, + { + "name": "literal-sample-128-fast-check-v4", + "export": "literalSample128", + "scenario": "arbitrary-literal-sample-128", + "operation": "sample", + "size": 128 + }, + { + "name": "map-sample-128-fast-check-v4", + "export": "mapSample128", + "scenario": "arbitrary-map-sample-128", + "operation": "sample-map", + "size": 128 + }, + { + "name": "passing-filter-sample-128-fast-check-v4", + "export": "passingFilterSample128", + "scenario": "arbitrary-passing-filter-sample-128", + "operation": "sample-filter", + "size": 128 + }, + { + "name": "selective-filter-sample-32-fast-check-v4", + "export": "selectiveFilterSample32", + "scenario": "arbitrary-selective-filter-sample-32", + "operation": "sample-filter", + "size": 32 + }, + { + "name": "filter-map-sample-128-fast-check-v4", + "export": "filterMapSample128", + "scenario": "arbitrary-filter-map-sample-128", + "operation": "sample-filter-map", + "size": 128 + }, + { + "name": "filter-check-falsify-shrink-fast-check-v4", + "export": "filterCheckFalsifyAndShrink", + "scenario": "arbitrary-filter-check-falsify-shrink", + "operation": "check-filter-and-shrink", + "path": "failure", + "size": 1 + }, + { + "name": "all-tuple-sample-128-fast-check-v4", + "export": "allTupleSample128", + "scenario": "arbitrary-all-tuple-sample-128", + "operation": "sample-all-tuple", + "size": 128 + }, + { + "name": "all-record-sample-128-fast-check-v4", + "export": "allRecordSample128", + "scenario": "arbitrary-all-record-sample-128", + "operation": "sample-all-record", + "size": 128 + }, + { + "name": "flat-map-sample-128-fast-check-v4", + "export": "flatMapSample128", + "scenario": "arbitrary-flat-map-sample-128", + "operation": "sample-flat-map", + "size": 128 + }, + { + "name": "flat-map-check-falsify-shrink-fast-check-v4", + "export": "flatMapCheckFalsifyAndShrink", + "scenario": "arbitrary-flat-map-check-falsify-shrink", + "operation": "check-flat-map-and-shrink", + "path": "failure", + "size": 1 + }, + { + "name": "flat-map-check-replay-fast-check-v4", + "export": "flatMapCheckReplay", + "scenario": "arbitrary-flat-map-check-replay", + "operation": "replay-flat-map", + "path": "failure", + "size": 1 + }, + { + "name": "check-pass-100-fast-check-v4", + "export": "checkPass100", + "scenario": "arbitrary-check-pass-100", + "operation": "check", + "size": 100 + }, + { + "name": "test-schema-verify-generation-100-fast-check-v4", + "export": "testSchemaVerifyGeneration100", + "scenario": "test-schema-verify-generation-100", + "operation": "derive-and-check", + "size": 100 + }, + { + "name": "check-falsify-shrink-fast-check-v4", + "export": "checkFalsifyAndShrink", + "scenario": "arbitrary-check-falsify-shrink", + "operation": "check-and-shrink", + "path": "failure", + "size": 1 + }, + { + "name": "check-replay-fast-check-v4", + "export": "checkReplay", + "scenario": "arbitrary-check-replay", + "operation": "replay", + "path": "failure", + "size": 1 + } + ] + } + ] + }, { "name": "schema", "fixtures": [ diff --git a/repos/effect/packages/effect/runtimeperf/run.mts b/repos/effect/packages/effect/runtimeperf/run.mts index e1f38f1636..b97665f60b 100644 --- a/repos/effect/packages/effect/runtimeperf/run.mts +++ b/repos/effect/packages/effect/runtimeperf/run.mts @@ -31,7 +31,7 @@ Options: --warmup-time --tier <0-3> --family - --implementation + --implementation ` const rotate = (items, offset) => items.map((_, index) => items[(index + offset) % items.length]) diff --git a/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts b/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts new file mode 100644 index 0000000000..374da26c4d --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts @@ -0,0 +1,414 @@ +import * as BigDecimal from "effect/BigDecimal" +import * as DateTime from "effect/DateTime" +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" +import * as FastCheck from "fast-check" +import assert from "node:assert/strict" +import type { Tree } from "./schema.ts" +import { + makeBigDecimalSchema, + makeDateTimeUtcSchema, + makeDateTimeZonedSchema, + makeOptionalStructSchema, + regularExpression, + validateNumbers, + validateRegExpCoverage, + validateRegExpValues, + validateSchemaValues, + validateStrings, + validateTrees, + validateUint8Arrays +} from "./schema.ts" + +const seed = 42 +const recursiveSeed = 188 +const regExpShrinkSeed = 0 +const namedTimeZones = ["UTC", "Europe/London", "America/New_York", "Asia/Tokyo", "Australia/Sydney"] as const + +const makeRegExpArbitrary = () => FastCheck.stringMatching(regularExpression) + +interface FlatMapValue { + readonly length: number + readonly values: ReadonlyArray +} + +const flatMapTargets = globalThis.Array.from({ length: 8 }, (_, index) => { + const length = index + 1 + return FastCheck.array(FastCheck.integer({ min: -1_000, max: 1_000 }), { minLength: length, maxLength: length }) + .map((values): FlatMapValue => ({ length, values })) +}) +const flatMapArbitrary = FastCheck.integer({ min: 1, max: 8 }).chain((length) => flatMapTargets[length - 1]) + +const scoreArbitrary = FastCheck.oneof( + FastCheck.constant(Option.none()), + FastCheck.integer({ min: 0, max: 100 }).map(Option.some) +) + +const treeFields = ( + score: FastCheck.Arbitrary>, + children: FastCheck.Arbitrary> +) => ({ + label: FastCheck.string({ minLength: 2, maxLength: 12 }), + score, + children +}) + +const treeArbitrary = (maxDepth = 1) => { + const depthIdentifier = FastCheck.createDepthIdentifier() + const recursion = { maxDepth, depthIdentifier } + const recursive = FastCheck.letrec<{ readonly Tree: Tree }>((tie) => ({ + Tree: FastCheck.oneof( + recursion, + FastCheck.record(treeFields(FastCheck.constant(Option.none()), FastCheck.constant([]))), + FastCheck.constant(null).chain(() => + FastCheck.record( + treeFields( + FastCheck.oneof(recursion, FastCheck.constant(Option.none()), scoreArbitrary), + FastCheck.array(tie("Tree"), { maxLength: 3 }) + ) + ) + ) + ) + })).Tree + return FastCheck.record( + treeFields(scoreArbitrary, FastCheck.array(recursive, { maxLength: 3 })) + ) +} + +const timeZoneArbitrary = () => + FastCheck.oneof( + FastCheck.integer({ min: -12 * 60 * 60 * 1_000, max: 14 * 60 * 60 * 1_000 }).map(DateTime.zoneMakeOffset), + FastCheck.constantFrom(...namedTimeZones).map(DateTime.zoneMakeNamedUnsafe) + ) + +export const coldRecursiveFirstSample = () => ({ + run: () => FastCheck.sample(treeArbitrary(0), { numRuns: 1, seed }), + validate: validateTrees(1, 2, 2) +}) + +export const recursiveSample32 = () => { + const arbitrary = treeArbitrary() + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 32, seed: recursiveSeed }), + validate: validateTrees(32, 90, 110) + } +} + +export const optionalStructSample128 = () => { + const schema = makeOptionalStructSchema() + const item = FastCheck.integer({ min: 0, max: 1_000 }) + const arbitrary = FastCheck.record({ a: item, b: item, c: item, d: item, e: item, f: item, g: item, h: item }, { + requiredKeys: [] + }) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: validateSchemaValues(schema, 128) + } +} + +export const constrainedStringSample128 = () => { + const arbitrary = FastCheck.string({ minLength: 32, maxLength: 32 }) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: validateStrings(128) + } +} + +export const coldRegExpFirstSample = () => ({ + run: () => FastCheck.sample(makeRegExpArbitrary(), { numRuns: 1, seed }), + validate: validateRegExpValues(1) +}) + +export const regExpSample64 = () => { + const arbitrary = makeRegExpArbitrary() + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 64, seed }), + validate: validateRegExpCoverage + } +} + +export const regExpCheckFalsifyAndShrink = () => { + const property = FastCheck.property(makeRegExpArbitrary(), () => false) + return { + run: () => FastCheck.check(property, { numRuns: 1, seed: regExpShrinkSeed }), + validate: (result: FastCheck.RunDetails<[string]>) => { + assert.equal(result.failed, true) + if (!result.failed) return + validateRegExpValues(1)(result.counterexample) + assert.equal(result.numShrinks > 0, true) + } + } +} + +export const boundedNumberSample128 = () => { + const arbitrary = FastCheck.double({ min: 2, max: 4, noNaN: true }) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: validateNumbers(128) + } +} + +export const uint8ArraySample128 = () => { + const arbitrary = FastCheck.uint8Array({ maxLength: 10 }) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: validateUint8Arrays(128) + } +} + +export const bigDecimalSample128 = () => { + const schema = makeBigDecimalSchema() + const scale = 20 + const factor = BigInt(10) ** BigInt(17) + const arbitrary = FastCheck.bigInt({ + min: BigInt(1234) * factor + BigInt(1), + max: BigInt(1236) * factor - BigInt(1) + }).map((value) => BigDecimal.make(value, scale)) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: validateSchemaValues(schema, 128) + } +} + +export const dateTimeUtcSample128 = () => { + const schema = makeDateTimeUtcSchema() + const arbitrary = FastCheck.integer({ min: -1_000_000_000, max: 1_000_000_000 }).map(DateTime.makeUnsafe) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: validateSchemaValues(schema, 128) + } +} + +export const timeZoneNamedSample128 = () => { + const arbitrary = FastCheck.constantFrom(...namedTimeZones).map(DateTime.zoneMakeNamedUnsafe) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: validateSchemaValues(Schema.TimeZoneNamed, 128) + } +} + +export const timeZoneSample128 = () => { + const arbitrary = timeZoneArbitrary() + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: validateSchemaValues(Schema.TimeZone, 128) + } +} + +export const dateTimeZonedSample128 = () => { + const schema = makeDateTimeZonedSchema() + const arbitrary = FastCheck.tuple( + FastCheck.integer({ min: -1_000_000_000, max: 1_000_000_000 }), + timeZoneArbitrary() + ).map(([epochMilliseconds, timeZone]) => DateTime.makeZonedUnsafe(epochMilliseconds, { timeZone })) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: validateSchemaValues(schema, 128) + } +} + +export const rareFilterSample32 = () => { + const arbitrary = FastCheck.integer({ min: 0, max: 255 }).filter((value) => value % 16 === 0) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 32, seed }), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 32) + assert.equal(values.every((value) => value % 16 === 0), true) + } + } +} + +export const uniqueArraySample32 = () => { + const arbitrary = FastCheck.uniqueArray(FastCheck.integer({ min: 0, max: 1_023 }), { + minLength: 32, + maxLength: 32 + }) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 32, seed }), + validate: (values: ReadonlyArray>) => { + assert.equal(values.length, 32) + assert.equal(values.every((value) => value.length === 32 && new Set(value).size === 32), true) + } + } +} + +export const literalSample128 = () => { + const arbitrary = FastCheck.constant("value") + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => value === "value"), true) + } + } +} + +export const mapSample128 = () => { + const arbitrary = FastCheck.integer({ min: 0, max: 1_000 }).map((value) => value + 1) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => value >= 1 && value <= 1_001), true) + } + } +} + +export const passingFilterSample128 = () => { + const arbitrary = FastCheck.integer({ min: 0, max: 1_000 }).filter((value) => value >= 0) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => value >= 0 && value <= 1_000), true) + } + } +} + +export const selectiveFilterSample32 = () => { + const arbitrary = FastCheck.integer({ min: 0, max: 255 }).filter((value) => value % 16 === 0) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 32, seed }), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 32) + assert.equal(values.every((value) => value % 16 === 0), true) + } + } +} + +export const filterMapSample128 = () => { + const arbitrary = FastCheck.integer({ min: 0, max: 255 }) + .filter((value) => value % 2 === 0) + .map((value) => value / 2) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => Number.isInteger(value) && value >= 0 && value <= 127), true) + } + } +} + +export const filterCheckFalsifyAndShrink = () => { + const arbitrary = FastCheck.integer({ min: 1, max: 8 }).filter( + (value) => value === 8 || value === 5 || value === 4 + ) + const property = FastCheck.property(arbitrary, () => false) + return { + run: () => FastCheck.check(property, { examples: [[8]], numRuns: 1, seed }), + validate: (result: FastCheck.RunDetails<[number]>) => { + assert.equal(result.failed, true) + assert.deepEqual(result.counterexample, [4]) + assert.equal(result.numShrinks, 2) + } + } +} + +export const allTupleSample128 = () => { + const arbitrary = FastCheck.tuple(FastCheck.constant("left"), FastCheck.constant(1)) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: (values: ReadonlyArray<["left", 1]>) => { + assert.equal(values.length, 128) + assert.equal(values.every(([left, right]) => left === "left" && right === 1), true) + } + } +} + +export const allRecordSample128 = () => { + const arbitrary = FastCheck.record({ + name: FastCheck.constantFrom("Ada", "Grace"), + age: FastCheck.integer() + }) + return { + run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }), + validate: (values: ReadonlyArray<{ readonly name: string; readonly age: number }>) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => value.name === "Ada" || value.name === "Grace"), true) + } + } +} + +export const flatMapSample128 = () => ({ + run: () => FastCheck.sample(flatMapArbitrary, { numRuns: 128, seed }), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => value.values.length === value.length), true) + } +}) + +export const flatMapCheckFalsifyAndShrink = () => { + const property = FastCheck.property(flatMapArbitrary, () => false) + return { + run: () => FastCheck.check(property, { numRuns: 1, seed }), + validate: (result: FastCheck.RunDetails<[FlatMapValue]>) => { + assert.equal(result.failed, true) + assert.equal(result.counterexample?.[0].length, 1) + assert.equal(result.counterexample?.[0].values.length, 1) + } + } +} + +export const flatMapCheckReplay = () => { + const property = FastCheck.property(flatMapArbitrary, () => false) + const initial = FastCheck.check(property, { numRuns: 1, seed }) + assert.equal(initial.failed, true) + return { + run: () => FastCheck.check(property, { numRuns: 1, seed: initial.seed, path: initial.counterexamplePath }), + validate: (result: FastCheck.RunDetails<[FlatMapValue]>) => { + assert.equal(result.failed, true) + assert.equal(result.counterexample?.[0].length, 1) + assert.equal(result.counterexample?.[0].values.length, 1) + } + } +} + +export const checkPass100 = () => { + const arbitrary = FastCheck.integer() + const property = FastCheck.property(arbitrary, () => true) + return { + run: () => FastCheck.check(property, { numRuns: 100, seed }), + validate: (result: FastCheck.RunDetails<[number]>) => { + assert.equal(result.failed, false) + assert.equal(result.numRuns, 100) + assert.equal(result.numSkips, 0) + } + } +} + +export const testSchemaVerifyGeneration100 = () => ({ + run: () => { + const schema = Schema.Int + const arbitrary = FastCheck.integer() + FastCheck.assert(FastCheck.property(arbitrary, Schema.is(schema)), { numRuns: 100, seed }) + }, + validate: (result: void) => assert.equal(result, undefined) +}) + +export const checkFalsifyAndShrink = () => { + const arbitrary = FastCheck.integer({ min: 1, max: 1_000 }) + const property = FastCheck.property(arbitrary, (value) => value < 0) + return { + run: () => FastCheck.check(property, { examples: [[1_000]], numRuns: 1, seed }), + validate: (result: FastCheck.RunDetails<[number]>) => { + assert.equal(result.failed, true) + assert.deepEqual(result.counterexample, [1]) + assert.equal(result.numShrinks, 1) + } + } +} + +export const checkReplay = () => { + const arbitrary = FastCheck.integer({ min: 1, max: 1_000 }) + const property = FastCheck.property(arbitrary, (value) => value < 0) + const initial = FastCheck.check(property, { examples: [[1_000]], numRuns: 1, seed }) + assert.equal(initial.failed, true) + return { + run: () => FastCheck.check(property, { numRuns: 1, seed: initial.seed, path: initial.counterexamplePath }), + validate: (result: FastCheck.RunDetails<[number]>) => { + assert.equal(result.failed, true) + assert.deepEqual(result.counterexample, [1]) + assert.equal(result.numShrinks, 0) + } + } +} diff --git a/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts b/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts new file mode 100644 index 0000000000..79d46504b2 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts @@ -0,0 +1,438 @@ +import * as Effect from "effect/Effect" +import * as Result from "effect/Result" +import * as Schema from "effect/Schema" +import * as TestSchema from "effect/testing/TestSchema" +import * as Arbitrary from "effect/unstable/arbitrary/Arbitrary" +import assert from "node:assert/strict" +import { + makeBigDecimalSchema, + makeConstrainedStringSchema, + makeDateTimeUtcSchema, + makeDateTimeZonedSchema, + makeOptionalStructSchema, + makeRareFilterSchema, + makeRegExpSchema, + makeTreeSchema, + makeUniqueArraySchema, + regularExpression, + validateNumbers, + validateRegExpCoverage, + validateRegExpValues, + validateSchemaValues, + validateStrings, + validateTrees, + validateUint8Arrays +} from "./schema.ts" + +const seed = 42 +const recursiveSeed = 188 +const regExpShrinkSeed = 0 +const size = 10 + +interface FlatMapValue { + readonly length: number + readonly values: ReadonlyArray +} + +const flatMapSource = Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 8 }))) +const flatMapItem = Schema.Int.check(Schema.isBetween({ minimum: -1_000, maximum: 1_000 })) +const flatMapTargets = globalThis.Array.from({ length: 8 }, (_, index) => { + const length = index + 1 + return Arbitrary.schema( + Schema.Array(flatMapItem).check(Schema.isMinLength(length), Schema.isMaxLength(length)) + ).pipe(Arbitrary.map((values): FlatMapValue => ({ length, values }))) +}) +const makeFlatMapArbitrary = () => flatMapSource.pipe(Arbitrary.flatMap((length) => flatMapTargets[length - 1])) + +export const coldRecursiveFirstSample = () => ({ + run: () => + Effect.runSync( + Arbitrary.sampleEffect(Arbitrary.schema(makeTreeSchema()), { count: 1, seed, size: 1 }) + ), + validate: validateTrees(1, 2, 2) +}) + +export const recursiveSample32 = () => { + const arbitrary = Arbitrary.schema(makeTreeSchema()) + const program = Arbitrary.sampleEffect(arbitrary, { count: 32, seed: recursiveSeed, size: 3 }) + return { + run: () => Effect.runSync(program), + validate: validateTrees(32, 90, 110) + } +} + +export const optionalStructSample128 = () => { + const schema = makeOptionalStructSchema() + const arbitrary = Arbitrary.schema(schema) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size: 8 }) + return { + run: () => Effect.runSync(program), + validate: validateSchemaValues(schema, 128) + } +} + +export const constrainedStringSample128 = () => { + const arbitrary = Arbitrary.schema(makeConstrainedStringSchema()) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: validateStrings(128) + } +} + +export const coldRegExpFirstSample = () => ({ + run: () => + Effect.runSync( + Arbitrary.sampleEffect(Arbitrary.schema(makeRegExpSchema()), { + count: 1, + maxDiscards: 0, + seed, + size: 48 + }) + ), + validate: validateRegExpValues(1) +}) + +export const regExpSample64 = () => { + const arbitrary = Arbitrary.schema(makeRegExpSchema()) + const program = Arbitrary.sampleEffect(arbitrary, { count: 64, maxDiscards: 0, seed, size: 48 }) + return { + run: () => Effect.runSync(program), + validate: validateRegExpCoverage + } +} + +export const regExpCheckFalsifyAndShrink = () => { + const arbitrary = Arbitrary.schema(makeRegExpSchema()) + const program = Arbitrary.checkEffect(arbitrary, () => false, { + runs: 1, + maxDiscards: 0, + seed: regExpShrinkSeed, + size: 48 + }) + return { + run: () => Effect.runSync(program), + validate: (result: Arbitrary.CheckResult) => { + assert.equal(result._tag, "Falsified") + if (result._tag !== "Falsified") return + validateRegExpValues(2)([result.initialInput, result.shrunkInput]) + assert.equal(result.shrinks > 0, true) + } + } +} + +export const boundedNumberSample128 = () => { + const arbitrary = Arbitrary.schema(Schema.Number.check(Schema.isBetween({ minimum: 2, maximum: 4 }))) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: validateNumbers(128) + } +} + +export const uint8ArraySample128 = () => { + const arbitrary = Arbitrary.schema(Schema.Uint8Array) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: validateUint8Arrays(128) + } +} + +export const bigDecimalSample128 = () => { + const schema = makeBigDecimalSchema() + const arbitrary = Arbitrary.schema(schema) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: validateSchemaValues(schema, 128) + } +} + +export const dateTimeUtcSample128 = () => { + const schema = makeDateTimeUtcSchema() + const arbitrary = Arbitrary.schema(schema) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: validateSchemaValues(schema, 128) + } +} + +export const timeZoneNamedSample128 = () => { + const arbitrary = Arbitrary.schema(Schema.TimeZoneNamed) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: validateSchemaValues(Schema.TimeZoneNamed, 128) + } +} + +export const timeZoneSample128 = () => { + const arbitrary = Arbitrary.schema(Schema.TimeZone) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: validateSchemaValues(Schema.TimeZone, 128) + } +} + +export const dateTimeZonedSample128 = () => { + const schema = makeDateTimeZonedSchema() + const arbitrary = Arbitrary.schema(schema) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: validateSchemaValues(schema, 128) + } +} + +export const rareFilterSample32 = () => { + const arbitrary = Arbitrary.schema(makeRareFilterSchema()) + const program = Arbitrary.sampleEffect(arbitrary, { count: 32, maxDiscards: 2_048, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 32) + assert.equal(values.every((value) => value % 16 === 0), true) + } + } +} + +export const uniqueArraySample32 = () => { + const arbitrary = Arbitrary.schema(makeUniqueArraySchema()) + const program = Arbitrary.sampleEffect(arbitrary, { count: 32, maxDiscards: 2_048, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (values: ReadonlyArray>) => { + assert.equal(values.length, 32) + assert.equal(values.every((value) => value.length === 32 && new Set(value).size === 32), true) + } + } +} + +export const literalSample128 = () => { + const arbitrary = Arbitrary.schema(Schema.Literal("value")) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => value === "value"), true) + } + } +} + +export const mapSample128 = () => { + const arbitrary = Arbitrary.map( + Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 1_000 }))), + (value) => value + 1 + ) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => value >= 1 && value <= 1_001), true) + } + } +} + +export const passingFilterSample128 = () => { + const arbitrary = Arbitrary.filter( + Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 1_000 }))), + (value) => value >= 0 + ) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => value >= 0 && value <= 1_000), true) + } + } +} + +export const selectiveFilterSample32 = () => { + const arbitrary = Arbitrary.filter( + Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 255 }))), + (value) => value % 16 === 0 + ) + const program = Arbitrary.sampleEffect(arbitrary, { count: 32, maxDiscards: 2_048, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 32) + assert.equal(values.every((value) => value % 16 === 0), true) + } + } +} + +export const filterMapSample128 = () => { + const arbitrary = Arbitrary.filterMap( + Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 255 }))), + (value) => value % 2 === 0 ? Result.succeed(value / 2) : Result.fail(value) + ) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 2_048, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => Number.isInteger(value) && value >= 0 && value <= 127), true) + } + } +} + +export const filterCheckFalsifyAndShrink = () => { + const arbitrary = Arbitrary.filter( + Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 8 }))), + (value) => value === 8 || value === 5 || value === 4 + ) + const program = Arbitrary.checkEffect(arbitrary, () => false, { + runs: 1, + seed: 47, + size, + maxShrinks: 100 + }) + return { + run: () => Effect.runSync(program), + validate: (result: Arbitrary.CheckResult) => { + assert.equal(result._tag, "Falsified") + if (result._tag !== "Falsified") return + assert.equal(result.initialInput, 8) + assert.equal(result.shrunkInput, 4) + assert.equal(result.shrinks, 2) + } + } +} + +export const allTupleSample128 = () => { + const arbitrary = Arbitrary.all([ + Arbitrary.schema(Schema.Literal("left")), + Arbitrary.schema(Schema.Literal(1)) + ]) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (values: ReadonlyArray<["left", 1]>) => { + assert.equal(values.length, 128) + assert.equal(values.every(([left, right]) => left === "left" && right === 1), true) + } + } +} + +export const allRecordSample128 = () => { + const arbitrary = Arbitrary.all({ + name: Arbitrary.schema(Schema.Literals(["Ada", "Grace"])), + age: Arbitrary.schema(Schema.Int) + }) + const program = Arbitrary.sampleEffect(arbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (values: ReadonlyArray<{ readonly name: string; readonly age: number }>) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => value.name === "Ada" || value.name === "Grace"), true) + } + } +} + +export const flatMapSample128 = () => { + const flatMapArbitrary = makeFlatMapArbitrary() + const program = Arbitrary.sampleEffect(flatMapArbitrary, { count: 128, maxDiscards: 0, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (values: ReadonlyArray) => { + assert.equal(values.length, 128) + assert.equal(values.every((value) => value.values.length === value.length), true) + } + } +} + +export const flatMapCheckFalsifyAndShrink = () => { + const flatMapArbitrary = makeFlatMapArbitrary() + const program = Arbitrary.checkEffect(flatMapArbitrary, () => false, { runs: 1, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (result: Arbitrary.CheckResult) => { + assert.equal(result._tag, "Falsified") + if (result._tag !== "Falsified") return + assert.equal(result.shrunkInput.length, 1) + assert.equal(result.shrunkInput.values.length, 1) + } + } +} + +export const flatMapCheckReplay = () => { + const flatMapArbitrary = makeFlatMapArbitrary() + const property = () => false + const initial = Effect.runSync(Arbitrary.checkEffect(flatMapArbitrary, property, { runs: 1, seed, size })) + assert.equal(initial._tag, "Falsified") + if (initial._tag !== "Falsified") throw new Error("Expected the flatMap replay setup to falsify") + const program = Arbitrary.checkEffect(flatMapArbitrary, property, { replay: initial.replay }) + return { + run: () => Effect.runSync(program), + validate: (result: Arbitrary.CheckResult) => { + assert.equal(result._tag, "Falsified") + if (result._tag !== "Falsified") return + assert.equal(result.shrunkInput.length, 1) + assert.equal(result.shrunkInput.values.length, 1) + } + } +} + +export const checkPass100 = () => { + const arbitrary = Arbitrary.schema(Schema.Int) + const program = Arbitrary.checkEffect(arbitrary, () => true, { runs: 100, seed, size }) + return { + run: () => Effect.runSync(program), + validate: (result: Arbitrary.CheckResult) => { + assert.deepEqual(result, { _tag: "Passed", runs: 100, discards: 0 }) + } + } +} + +export const testSchemaVerifyGeneration100 = () => { + const asserts = new TestSchema.Asserts(Schema.Int) + return { + run: () => asserts.arbitrary().verifyGeneration({ runs: 100, seed }), + validate: (result: void) => assert.equal(result, undefined) + } +} + +export const checkFalsifyAndShrink = () => { + const arbitrary = Arbitrary.schema( + Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000 })) + ) + const program = Arbitrary.checkEffect(arbitrary, (value) => value < 0, { runs: 1, seed: 47, size }) + return { + run: () => Effect.runSync(program), + validate: (result: Arbitrary.CheckResult) => { + assert.equal(result._tag, "Falsified") + if (result._tag !== "Falsified") return + assert.equal(result.initialInput, 1_000) + assert.equal(result.shrunkInput, 1) + assert.equal(result.shrinks, 1) + } + } +} + +export const checkReplay = () => { + const arbitrary = Arbitrary.schema( + Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000 })) + ) + const property = (value: number) => value < 0 + const initial = Effect.runSync(Arbitrary.checkEffect(arbitrary, property, { runs: 1, seed: 47, size })) + assert.equal(initial._tag, "Falsified") + if (initial._tag !== "Falsified") throw new Error("Expected the replay setup to falsify") + const program = Arbitrary.checkEffect(arbitrary, property, { replay: initial.replay }) + return { + run: () => Effect.runSync(program), + validate: (result: Arbitrary.CheckResult) => { + assert.equal(result._tag, "Falsified") + if (result._tag !== "Falsified") return + assert.equal(result.initialInput, 1_000) + assert.equal(result.shrunkInput, 1) + assert.equal(result.shrinks, 1) + } + } +} diff --git a/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/schema.ts b/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/schema.ts new file mode 100644 index 0000000000..b71ad8a569 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/schema.ts @@ -0,0 +1,134 @@ +import * as BigDecimal from "effect/BigDecimal" +import * as DateTime from "effect/DateTime" +import type * as Option from "effect/Option" +import * as Schema from "effect/Schema" +import assert from "node:assert/strict" + +export interface Tree { + readonly label: string + readonly score: Option.Option + readonly children: ReadonlyArray +} + +export const makeTreeSchema = (): Schema.Codec => { + const Tree: Schema.Codec = Schema.Struct({ + label: Schema.String.check(Schema.isMinLength(2), Schema.isMaxLength(12)), + score: Schema.Option(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 }))), + children: Schema.Array(Schema.suspend(() => Tree)).check(Schema.isMaxLength(3)) + }) + return Tree +} + +export const makeConstrainedStringSchema = () => Schema.String.check(Schema.isMinLength(32), Schema.isMaxLength(32)) + +const optionalInt = Schema.optionalKey(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 1_000 }))) + +export const makeOptionalStructSchema = () => + Schema.Struct({ + a: optionalInt, + b: optionalInt, + c: optionalInt, + d: optionalInt, + e: optionalInt, + f: optionalInt, + g: optionalInt, + h: optionalInt + }) + +export const regularExpression = /^(?:a|[B-D]{2}|[0-9]{3}){16}$/ + +export const makeRegExpSchema = () => Schema.String.check(Schema.isPattern(regularExpression)) + +export const makeRareFilterSchema = () => + Schema.Int.check( + Schema.isBetween({ minimum: 0, maximum: 255 }), + Schema.makeFilter((value: number) => value % 16 === 0) + ) + +export const makeUniqueArraySchema = () => + Schema.UniqueArray(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 1_023 }))).check( + Schema.isMinLength(32), + Schema.isMaxLength(32) + ) + +export const makeBigDecimalSchema = () => + Schema.BigDecimal.check(Schema.isBetweenBigDecimal({ + minimum: BigDecimal.make(BigInt(1234), 3), + maximum: BigDecimal.make(BigInt(1236), 3), + exclusiveMinimum: true, + exclusiveMaximum: true + })) + +const isBetweenDateTime = Schema.makeIsBetween({ order: DateTime.Order }) + +export const makeDateTimeUtcSchema = () => + Schema.DateTimeUtc.check(isBetweenDateTime({ + minimum: DateTime.makeUnsafe(-1_000_000_000), + maximum: DateTime.makeUnsafe(1_000_000_000) + })) + +export const makeDateTimeZonedSchema = () => + Schema.DateTimeZoned.check(isBetweenDateTime({ + minimum: DateTime.makeZonedUnsafe(-1_000_000_000, { timeZone: "UTC" }), + maximum: DateTime.makeZonedUnsafe(1_000_000_000, { timeZone: "UTC" }) + })) + +const validateTree = Schema.is(makeTreeSchema()) + +const countTreeNodes = (tree: Tree): number => + 1 + tree.children.reduce((total, child) => total + countTreeNodes(child), 0) + +export const validateTrees = (count: number, minimumNodes: number, maximumNodes: number) => (values: unknown) => { + assert.ok(Array.isArray(values)) + assert.equal(values.length, count) + assert.equal(values.every(validateTree), true) + const nodes = values.reduce((total, tree) => total + countTreeNodes(tree), 0) + assert.ok(nodes >= minimumNodes && nodes <= maximumNodes) +} + +export const validateStrings = (count: number) => (values: unknown) => { + assert.ok(Array.isArray(values)) + assert.equal(values.length, count) + assert.equal(values.every((value) => typeof value === "string" && value.length === 32), true) +} + +export const validateRegExpValues = (count: number) => (values: unknown) => { + assert.ok(Array.isArray(values)) + assert.equal(values.length, count) + assert.equal( + values.every((value) => typeof value === "string" && regularExpression.test(value)), + true + ) +} + +export const validateRegExpCoverage = (values: unknown) => { + validateRegExpValues(64)(values) + assert.ok(Array.isArray(values)) + assert.ok(new Set(values.map((value) => value.length)).size > 1) + assert.equal(values.some((value) => value.includes("a")), true) + assert.equal(values.some((value) => /[B-D]{2}/.test(value)), true) + assert.equal(values.some((value) => /[0-9]{3}/.test(value)), true) +} + +export const validateNumbers = (count: number) => (values: unknown) => { + assert.ok(Array.isArray(values)) + assert.equal(values.length, count) + assert.equal(values.every((value) => typeof value === "number" && value >= 2 && value <= 4), true) +} + +export const validateUint8Arrays = (count: number) => (values: unknown) => { + assert.ok(Array.isArray(values)) + assert.equal(values.length, count) + assert.equal(values.every((value) => value instanceof Uint8Array && value.length <= 10), true) + const bytes = values.reduce((total, value) => total + value.length, 0) + assert.ok(bytes >= 500 && bytes <= 700) +} + +export const validateSchemaValues = (schema: Schema.Top, count: number) => { + const is = Schema.is(schema) + return (values: unknown) => { + assert.ok(Array.isArray(values)) + assert.equal(values.length, count) + assert.equal(values.every(is), true) + } +} diff --git a/repos/effect/packages/effect/runtimeperf/test/registry.test.mts b/repos/effect/packages/effect/runtimeperf/test/registry.test.mts index e43831bc84..1598b63440 100644 --- a/repos/effect/packages/effect/runtimeperf/test/registry.test.mts +++ b/repos/effect/packages/effect/runtimeperf/test/registry.test.mts @@ -9,7 +9,29 @@ describe("runtimeperf registry", () => { const { fixtures } = loadRegistry() assert.equal(new Set(fixtures.map((fixture) => fixture.target)).size, fixtures.length) for (const fixture of fixtures) { - assert.ok(["effect", "valibot", "zod4"].includes(fixture.implementation)) + assert.ok(["effect", "fast-check-v4", "valibot", "zod4"].includes(fixture.implementation)) + } + }) + + it("pairs every Arbitrary scenario across the native and fast-check implementations", () => { + const { fixtures } = loadRegistry() + const scenarios = Map.groupBy( + fixtures.filter((fixture) => fixture.suite === "arbitrary"), + (fixture) => fixture.scenario + ) + assert.equal(scenarios.size, 31) + for (const fixtures of scenarios.values()) { + assert.deepEqual(fixtures.map((fixture) => fixture.implementation).sort(), ["effect", "fast-check-v4"]) + const metadata = (fixture) => ({ + export: fixture.export, + family: fixture.family, + operation: fixture.operation, + path: fixture.path, + scenario: fixture.scenario, + size: fixture.size, + tier: fixture.tier + }) + assert.deepEqual(metadata(fixtures[0]), metadata(fixtures[1])) } }) diff --git a/repos/effect/packages/effect/runtimeperf/utils.mts b/repos/effect/packages/effect/runtimeperf/utils.mts index a146ac9dbd..2dc6210b02 100644 --- a/repos/effect/packages/effect/runtimeperf/utils.mts +++ b/repos/effect/packages/effect/runtimeperf/utils.mts @@ -25,6 +25,7 @@ export const hashFile = (path) => sha256(readFileSync(path)) export const libraryVersions = () => ({ effect: readJson(join(effectDir, "package.json")).version, + fastCheck: readJson(join(repoRoot, "node_modules", "fast-check", "package.json")).version, tinybench: readJson(join(effectDir, "node_modules", "tinybench", "package.json")).version, valibot: readJson(join(effectDir, "node_modules", "valibot", "package.json")).version, zod: readJson(join(repoRoot, "node_modules", "zod", "package.json")).version, diff --git a/repos/effect/packages/effect/src/Array.ts b/repos/effect/packages/effect/src/Array.ts index 9e594cb808..8813ded8ce 100644 --- a/repos/effect/packages/effect/src/Array.ts +++ b/repos/effect/packages/effect/src/Array.ts @@ -16,6 +16,7 @@ import { dual, identity } from "./Function.ts" import * as Hash from "./Hash.ts" import type { TypeLambda } from "./HKT.ts" import * as internalArray from "./internal/array.ts" +import * as Count from "./internal/count.ts" import * as internalDoNotation from "./internal/doNotation.ts" import * as InternalRecord from "./internal/record.ts" import * as moduleIterable from "./Iterable.ts" @@ -158,6 +159,7 @@ export const make = >( * * **Details** * + * `n` is rounded down. `NaN` and non-positive values are treated as `0`. * Elements are typed as `A | undefined` because the slots are empty. * * **Example** (Allocating a fixed-size array) @@ -173,7 +175,7 @@ export const make = >( * @category constructors * @since 2.0.0 */ -export const allocate = (n: number): Array => new Array(n) +export const allocate = (n: number): Array => new Array(Count.normalize(n)) /** * Creates a `NonEmptyArray` of length `n` where element `i` is computed by `f(i)`. @@ -184,9 +186,9 @@ export const allocate = (n: number): Array => new Arra * * **Details** * - * `n` is normalized to an integer greater than or equal to 1, so this function - * always returns at least one element. Supports both data-first and data-last - * usage. + * `n` is rounded down and normalized to an integer greater than or equal to 1. + * `NaN` is treated as `1`, so this function always returns at least one + * element. Supports both data-first and data-last usage. * * **Example** (Generating values from indices) * @@ -206,7 +208,7 @@ 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 max = Count.normalizeNonEmpty(n) const out = new Array(max) for (let i = 0; i < max; i++) { out[i] = f(i) @@ -907,13 +909,23 @@ export const isReadonlyArrayNonEmpty: (self: ReadonlyArray) => self is Non */ export const length = (self: ReadonlyArray): number => self.length +/** + * Checks whether a string represents a JavaScript array index: a non-negative + * integer below `2 ** 32 - 1`, written without leading zeroes, a sign, or + * exponent notation. + * + * @internal + */ +export function isCanonicalArrayIndex(key: string): boolean { + const index = Number(key) + return String(index) === key && Number.isInteger(index) && index >= 0 && index < 2 ** 32 - 1 +} + /** @internal */ export function isOutOfBounds(i: number, as: ReadonlyArray): boolean { return !Number.isFinite(i) || i < 0 || i >= as.length } -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. @@ -1260,6 +1272,8 @@ export function init(self: Iterable): Option.Option> { */ export const initNonEmpty = (self: NonEmptyReadonlyArray): Array => self.slice(0, -1) +const clampCount = (n: number, length: number): number => Math.min(Count.normalize(n), length) + /** * Keeps the first `n` elements, creating a new array. * @@ -1269,7 +1283,8 @@ export const initNonEmpty = (self: NonEmptyReadonlyArray): Array => sel * * **Details** * - * `n` is clamped to `[0, length]`. Returns an empty array when `n <= 0`. + * `n` is rounded down and clamped to `[0, length]`. `NaN` is treated as `0`. + * Returns an empty array when `n <= 0`. * * **Example** (Taking from the start) * @@ -1291,7 +1306,7 @@ export const take: { (self: Iterable, n: number): Array } = dual(2, (self: Iterable, n: number): Array => { const input = fromIterable(self) - return input.slice(0, clamp(n, input)) + return input.slice(0, clampCount(n, input.length)) }) /** @@ -1303,7 +1318,8 @@ export const take: { * * **Details** * - * `n` is clamped to `[0, length]`. Returns an empty array when `n <= 0`. + * `n` is rounded down and clamped to `[0, length]`. `NaN` is treated as `0`. + * Returns an empty array when `n <= 0`. * * **Example** (Taking from the end) * @@ -1324,7 +1340,7 @@ export const takeRight: { (self: Iterable, n: number): Array } = dual(2, (self: Iterable, n: number): Array => { const input = fromIterable(self) - const i = clamp(n, input) + const i = clampCount(n, input.length) return i === 0 ? [] : input.slice(-i) }) @@ -1480,8 +1496,8 @@ export const span: { * * **Details** * - * `n` is clamped to `[0, length]`. When `n <= 0`, this returns a copy of the - * full array. + * `n` is rounded down and clamped to `[0, length]`. `NaN` is treated as `0`. + * When `n <= 0`, this returns a copy of the full array. * * **Example** (Dropping from the start) * @@ -1503,7 +1519,7 @@ export const drop: { (self: Iterable, n: number): Array } = dual(2, (self: Iterable, n: number): Array => { const input = fromIterable(self) - return input.slice(clamp(n, input), input.length) + return input.slice(clampCount(n, input.length), input.length) }) /** @@ -1515,7 +1531,7 @@ export const drop: { * * **Details** * - * `n` is clamped to `[0, length]`. + * `n` is rounded down and clamped to `[0, length]`. `NaN` is treated as `0`. * * **Example** (Dropping from the end) * @@ -1536,7 +1552,7 @@ export const dropRight: { (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)) + return input.slice(0, input.length - clampCount(n, input.length)) }) /** @@ -2642,8 +2658,8 @@ export const chop: { * * **Details** * - * `n` can be `0`, in which case all elements are placed in the second array. - * The index is floored to an integer. + * `n` is rounded down and clamped to `[0, length]`. `NaN` is treated as `0`, + * which places all elements in the second array. * * **Example** (Splitting at an index) * @@ -2664,7 +2680,7 @@ export const splitAt: { (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) + const _n = Count.normalize(n) if (isReadonlyArrayNonEmpty(input)) { if (_n >= 1) { return splitAtNonEmpty(input, _n) @@ -2683,6 +2699,10 @@ export const splitAt: { * Use when downstream code requires the left side of the split to contain at * least one element. * + * **Details** + * + * `n` is rounded down and clamped to `[1, length]`. `NaN` is treated as `1`. + * * **Example** (Splitting a non-empty array) * * ```ts import.meta.vitest @@ -2700,7 +2720,7 @@ 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)) + const _n = Count.normalizeNonEmpty(n) return _n >= self.length ? [copy(self), []] : [prepend(self.slice(1, _n), headNonEmpty(self)), self.slice(_n)] @@ -2715,7 +2735,8 @@ export const splitAtNonEmpty: { * * **Details** * - * Uses `chunksOf(ceil(length / n))` internally. The last chunk may be shorter. + * `n` is rounded down and normalized to at least `1`, with `NaN` treated as + * `1`. The last chunk may be shorter. * * **Example** (Splitting into groups) * @@ -2735,7 +2756,7 @@ export const split: { (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))) + return chunksOf(input, Math.ceil(input.length / Count.normalizeNonEmpty(n))) }) /** @@ -2817,7 +2838,8 @@ export const copy: { * * **Details** * - * Returns an empty array when `n <= 0`. + * `n` is rounded down. `NaN` and non-positive values are treated as `0`, which + * returns an empty array. * * **Example** (Padding an array) * @@ -2842,12 +2864,13 @@ export const pad: { ) => 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) + const length = Count.normalize(n) + if (self.length >= length) { + return take(self, length) } return appendAll( self, - makeBy(n - self.length, () => fill) + makeBy(length - self.length, () => fill) ) }) @@ -2862,8 +2885,10 @@ export const pad: { * * **Details** * - * `chunksOf(n)([])` is `[]`, not `[[]]`. Each chunk is a `NonEmptyArray`, and - * the outer return type preserves `NonEmptyArray`. + * `n` is rounded down and normalized to at least `1`; `NaN` and non-positive + * values therefore produce singleton chunks. `chunksOf(n)([])` is `[]`, not + * `[[]]`. Each chunk is a `NonEmptyArray`, and the outer return type preserves + * `NonEmptyArray`. * * **Example** (Chunking an array) * @@ -2904,8 +2929,9 @@ export const chunksOf: { * * **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. + * `n` is rounded down, with `NaN` and non-positive values treated as `0`. + * Returns an empty array if the normalized size is `0` or exceeds the array + * length. Each window is a tuple of exactly the normalized size. * * **Example** (Creating sliding windows) * @@ -2928,10 +2954,11 @@ export const window: { (self: Iterable, n: N): Array> } = dual(2, (self: Iterable, n: number): Array> => { const input = fromIterable(self) - if (n > 0 && isReadonlyArrayNonEmpty(input)) { + const size = Count.normalize(n) + if (size > 0 && size <= input.length && isReadonlyArrayNonEmpty(input)) { return Array.from( - { length: input.length - (n - 1) }, - (_, index) => input.slice(index, index + n) + { length: input.length - (size - 1) }, + (_, index) => input.slice(index, index + size) ) } return [] diff --git a/repos/effect/packages/effect/src/BigDecimal.ts b/repos/effect/packages/effect/src/BigDecimal.ts index 865db4449b..f7d6604753 100644 --- a/repos/effect/packages/effect/src/BigDecimal.ts +++ b/repos/effect/packages/effect/src/BigDecimal.ts @@ -106,13 +106,17 @@ const BigDecimalProto: Omit = { export const isBigDecimal = (u: unknown): u is BigDecimal => hasProperty(u, TypeId) /** - * Creates a `BigDecimal` from a `bigint` value and a scale. + * Creates a `BigDecimal` from a `bigint` value and a safe integer scale. * * **When to use** * * Use to construct a decimal directly from its unscaled integer value and * decimal scale. * + * **Gotchas** + * + * Throws a `RangeError` if `scale` is not a safe integer. + * * **Example** (Creating decimals from bigint and scale) * * ```ts import.meta.vitest @@ -133,6 +137,9 @@ export const isBigDecimal = (u: unknown): u is BigDecimal => hasProperty(u, Type * @since 2.0.0 */ export const make = (value: bigint, scale: number): BigDecimal => { + if (!Number.isSafeInteger(scale)) { + throw new RangeError(`Scale must be a safe integer, got ${scale}`) + } const o = Object.create(BigDecimalProto) o.value = value o.scale = scale @@ -630,6 +637,46 @@ export const divideUnsafe: { return divideWithPrecision(self.value, that.value, scale, DEFAULT_PRECISION) }) +const MAX_COMPARISON_SCALE_ALIGNMENT = 100 +const comparisonPowersOfTen: Array = [bigint1] + +const compareBigInt = (self: bigint, that: bigint): Ordering => self === that ? 0 : self < that ? -1 : 1 + +const compareMagnitude = (self: BigDecimal, that: BigDecimal): Ordering => { + const selfDigits = `${self.value < bigint0 ? -self.value : self.value}` + const thatDigits = `${that.value < bigint0 ? -that.value : that.value}` + const exponentDifference = BigInt(selfDigits.length - thatDigits.length) - BigInt(self.scale) + BigInt(that.scale) + if (exponentDifference !== bigint0) return exponentDifference < bigint0 ? -1 : 1 + + const length = Math.max(selfDigits.length, thatDigits.length) + for (let i = 0; i < length; i++) { + const selfDigit = i < selfDigits.length ? selfDigits.charCodeAt(i) : 48 + const thatDigit = i < thatDigits.length ? thatDigits.charCodeAt(i) : 48 + if (selfDigit !== thatDigit) return selfDigit < thatDigit ? -1 : 1 + } + return 0 +} + +const compare = (self: BigDecimal, that: BigDecimal): Ordering => { + if (self.scale === that.scale) return compareBigInt(self.value, that.value) + + const selfSign = sign(self) + const thatSign = sign(that) + if (selfSign !== thatSign) return selfSign < thatSign ? -1 : 1 + if (selfSign === 0) return 0 + + const scaleDifference = self.scale - that.scale + const absoluteScaleDifference = Math.abs(scaleDifference) + if (absoluteScaleDifference > MAX_COMPARISON_SCALE_ALIGNMENT) { + return selfSign === -1 ? compareMagnitude(that, self) : compareMagnitude(self, that) + } + + const powerOfTen = comparisonPowersOfTen[absoluteScaleDifference] ??= bigint10 ** BigInt(absoluteScaleDifference) + return scaleDifference > 0 + ? compareBigInt(self.value, that.value * powerOfTen) + : compareBigInt(self.value * powerOfTen, that.value) +} + /** * Provides an `Order` instance for `BigDecimal` that allows comparing and sorting BigDecimal values. * @@ -655,22 +702,7 @@ export const divideUnsafe: { * @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) -}) +export const Order: order.Order = order.make(compare) /** * Returns `true` if the first argument is less than the second, otherwise `false`. @@ -1100,17 +1132,7 @@ export const remainderUnsafe: { * @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 -}) +export const Equivalence: Equ.Equivalence = Equ.make((self, that) => compare(self, that) === 0) /** * Checks whether two `BigDecimal`s are equal. diff --git a/repos/effect/packages/effect/src/ByteSize.ts b/repos/effect/packages/effect/src/ByteSize.ts new file mode 100644 index 0000000000..ae7dfc1125 --- /dev/null +++ b/repos/effect/packages/effect/src/ByteSize.ts @@ -0,0 +1,658 @@ +/** + * Represents exact, non-negative, integral byte counts. + * + * Decimal units use powers of 1,000 and binary units use powers of 1,024. + * + * @since 4.0.0 + */ +import * as BI from "./BigInt.ts" +import type * as Brand from "./Brand.ts" +import type * as Combiner from "./Combiner.ts" +import type * as Equ from "./Equivalence.ts" +import { dual } from "./Function.ts" +import * as Option from "./Option.ts" +import type * as order from "./Order.ts" +import type * as Reducer from "./Reducer.ts" + +const TypeId = "~effect/ByteSize" + +const bigint0 = BigInt(0) +const bigint1 = BigInt(1) +const decimalBase = BigInt(1000) +const binaryBase = BigInt(1024) + +/** + * Represents an exact, non-negative number of bytes. + * + * @category models + * @since 4.0.0 + */ +export type ByteSize = Brand.Branded + +/** + * Values accepted by byte-size decoding operations. + * + * @category models + * @since 4.0.0 + */ +export type Input = ByteSize | bigint | number | string + +/** + * Canonical decimal byte unit symbols. + * + * @category models + * @since 4.0.0 + */ +export type DecimalUnit = + | "B" + | "kB" + | "MB" + | "GB" + | "TB" + | "PB" + | "EB" + | "ZB" + | "YB" + | "RB" + | "QB" + +/** + * Canonical binary byte unit symbols. + * + * @category models + * @since 4.0.0 + */ +export type BinaryUnit = + | "B" + | "KiB" + | "MiB" + | "GiB" + | "TiB" + | "PiB" + | "EiB" + | "ZiB" + | "YiB" + +/** + * Canonical decimal and binary byte unit symbols. + * + * @category models + * @since 4.0.0 + */ +export type Unit = DecimalUnit | BinaryUnit + +/** + * Options controlling compact byte-size formatting. + * + * @category models + * @since 4.0.0 + */ +export type FormatOptions = + & { + readonly precision?: number | undefined + readonly trailingZeros?: boolean | undefined + } + & ( + | { + readonly system?: "decimal" | undefined + readonly unit?: DecimalUnit | undefined + } + | { + readonly system?: "binary" | undefined + readonly unit?: BinaryUnit | undefined + } + ) + +interface UnitInfo { + readonly symbol: Unit + readonly factor: bigint + readonly names: ReadonlyArray +} + +const decimalUnits: ReadonlyArray = [ + { symbol: "B", factor: bigint1, names: ["B", "byte", "bytes"] }, + { symbol: "kB", factor: decimalBase, names: ["kB", "kilobyte", "kilobytes"] }, + { symbol: "MB", factor: decimalBase ** BigInt(2), names: ["MB", "megabyte", "megabytes"] }, + { symbol: "GB", factor: decimalBase ** BigInt(3), names: ["GB", "gigabyte", "gigabytes"] }, + { symbol: "TB", factor: decimalBase ** BigInt(4), names: ["TB", "terabyte", "terabytes"] }, + { symbol: "PB", factor: decimalBase ** BigInt(5), names: ["PB", "petabyte", "petabytes"] }, + { symbol: "EB", factor: decimalBase ** BigInt(6), names: ["EB", "exabyte", "exabytes"] }, + { symbol: "ZB", factor: decimalBase ** BigInt(7), names: ["ZB", "zettabyte", "zettabytes"] }, + { symbol: "YB", factor: decimalBase ** BigInt(8), names: ["YB", "yottabyte", "yottabytes"] }, + { symbol: "RB", factor: decimalBase ** BigInt(9), names: ["RB", "ronnabyte", "ronnabytes"] }, + { symbol: "QB", factor: decimalBase ** BigInt(10), names: ["QB", "quettabyte", "quettabytes"] } +] + +const binaryUnits: ReadonlyArray = [ + decimalUnits[0], + { symbol: "KiB", factor: binaryBase, names: ["KiB", "kibibyte", "kibibytes"] }, + { symbol: "MiB", factor: binaryBase ** BigInt(2), names: ["MiB", "mebibyte", "mebibytes"] }, + { symbol: "GiB", factor: binaryBase ** BigInt(3), names: ["GiB", "gibibyte", "gibibytes"] }, + { symbol: "TiB", factor: binaryBase ** BigInt(4), names: ["TiB", "tebibyte", "tebibytes"] }, + { symbol: "PiB", factor: binaryBase ** BigInt(5), names: ["PiB", "pebibyte", "pebibytes"] }, + { symbol: "EiB", factor: binaryBase ** BigInt(6), names: ["EiB", "exbibyte", "exbibytes"] }, + { symbol: "ZiB", factor: binaryBase ** BigInt(7), names: ["ZiB", "zebibyte", "zebibytes"] }, + { symbol: "YiB", factor: binaryBase ** BigInt(8), names: ["YiB", "yobibyte", "yobibytes"] } +] + +const allUnits = [...decimalUnits, ...binaryUnits.slice(1)] +const unitsByName = new Map(allUnits.flatMap((unit) => unit.names.map((name) => [name, unit] as const))) +const unitsBySymbol = new Map(allUnits.map((unit) => [unit.symbol, unit] as const)) + +const make = (value: bigint): ByteSize => value as ByteSize + +/** + * The byte size containing zero bytes. + * + * @category constants + * @since 4.0.0 + */ +export const zero: ByteSize = make(bigint0) + +const invalid = (message: string): never => { + throw new Error(`Invalid ByteSize: ${message}`) +} + +const fromNumber = (input: number): ByteSize => { + if (!Number.isSafeInteger(input) || input < 0) { + return invalid(`expected a non-negative safe integer, received ${input}`) + } + return make(BigInt(input)) +} + +const fromQuantity = (quantity: number | bigint, unit: UnitInfo): ByteSize => { + if (typeof quantity === "bigint") { + if (quantity < bigint0) return invalid(`expected a non-negative quantity, received ${quantity}`) + return make(quantity * unit.factor) + } + const value = quantity * Number(unit.factor) + if (!Number.isSafeInteger(value) || value < 0) { + return invalid(`expected an exact non-negative safe-integer byte result, received ${quantity} ${unit.symbol}`) + } + return make(BigInt(value)) +} + +const parse = (input: string): ByteSize => { + const match = /^\s*(\d+)(?:\.(\d+))?\s*([A-Za-z]+)\s*$/.exec(input) + if (match === null) return invalid(`unsupported syntax ${JSON.stringify(input)}`) + const unit = unitsByName.get(match[3]) + if (unit === undefined) return invalid(`unsupported unit ${JSON.stringify(match[3])}`) + const fraction = match[2] ?? "" + const scale = BigInt(10) ** BigInt(fraction.length) + const numerator = BigInt(match[1] + fraction) * unit.factor + if (numerator % scale !== bigint0) { + return invalid(`${JSON.stringify(input)} does not represent an integral number of bytes`) + } + return make(numerator / scale) +} + +/** + * Decodes a trusted input into a byte size and throws for invalid input. + * + * @category constructors + * @since 4.0.0 + */ +export const fromInputUnsafe = (input: Input): ByteSize => { + switch (typeof input) { + case "bigint": + if (input < bigint0) return invalid(`expected a non-negative bigint, received ${input}`) + return make(input) + case "number": + return fromNumber(input) + case "string": + return parse(input) + } + return invalid(`unsupported input ${input}`) +} + +/** + * Decodes an input into a byte size, returning `None` for invalid input. + * + * @category constructors + * @since 4.0.0 + */ +export const fromInput: (input: Input) => Option.Option = Option.liftThrowable(fromInputUnsafe) + +/** + * Creates a byte size from a non-negative byte count. + * + * @category constructors + * @since 4.0.0 + */ +export const bytes = (value: number | bigint): ByteSize => + typeof value === "bigint" ? fromInputUnsafe(value) : fromNumber(value) + +const unitConstructor = (symbol: Unit) => (value: number | bigint): ByteSize => + fromQuantity(value, unitsBySymbol.get(symbol)!) + +/** + * Creates a decimal kilobyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const kilobytes: (value: number | bigint) => ByteSize = unitConstructor("kB") +/** + * Creates a decimal megabyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const megabytes: (value: number | bigint) => ByteSize = unitConstructor("MB") +/** + * Creates a decimal gigabyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const gigabytes: (value: number | bigint) => ByteSize = unitConstructor("GB") +/** + * Creates a decimal terabyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const terabytes: (value: number | bigint) => ByteSize = unitConstructor("TB") +/** + * Creates a decimal petabyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const petabytes: (value: number | bigint) => ByteSize = unitConstructor("PB") +/** + * Creates a decimal exabyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const exabytes: (value: number | bigint) => ByteSize = unitConstructor("EB") +/** + * Creates a decimal zettabyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const zettabytes: (value: number | bigint) => ByteSize = unitConstructor("ZB") +/** + * Creates a decimal yottabyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const yottabytes: (value: number | bigint) => ByteSize = unitConstructor("YB") +/** + * Creates a decimal ronnabyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const ronnabytes: (value: number | bigint) => ByteSize = unitConstructor("RB") +/** + * Creates a decimal quettabyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const quettabytes: (value: number | bigint) => ByteSize = unitConstructor("QB") +/** + * Creates a binary kibibyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const kibibytes: (value: number | bigint) => ByteSize = unitConstructor("KiB") +/** + * Creates a binary mebibyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const mebibytes: (value: number | bigint) => ByteSize = unitConstructor("MiB") +/** + * Creates a binary gibibyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const gibibytes: (value: number | bigint) => ByteSize = unitConstructor("GiB") +/** + * Creates a binary tebibyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const tebibytes: (value: number | bigint) => ByteSize = unitConstructor("TiB") +/** + * Creates a binary pebibyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const pebibytes: (value: number | bigint) => ByteSize = unitConstructor("PiB") +/** + * Creates a binary exbibyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const exbibytes: (value: number | bigint) => ByteSize = unitConstructor("EiB") +/** + * Creates a binary zebibyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const zebibytes: (value: number | bigint) => ByteSize = unitConstructor("ZiB") +/** + * Creates a binary yobibyte value. + * + * @category constructors + * @since 4.0.0 + */ +export const yobibytes: (value: number | bigint) => ByteSize = unitConstructor("YiB") + +/** + * Checks whether a value is a byte size. + * + * @category guards + * @since 4.0.0 + */ +export const isByteSize = (input: unknown): input is ByteSize => typeof input === "bigint" && input >= bigint0 + +/** + * Checks whether a byte size is zero. + * + * @category predicates + * @since 4.0.0 + */ +export const isZero = (self: ByteSize): boolean => self === bigint0 + +/** + * Returns the exact byte count as a bigint. + * + * @category getters + * @since 4.0.0 + */ +export const toBigInt = (self: ByteSize): bigint => self + +/** + * Converts a byte size to a safe integer, returning `None` when it is too large. + * + * @category converting + * @since 4.0.0 + */ +export const toNumber: (self: ByteSize) => Option.Option = BI.toNumber + +/** + * Converts a byte size to a safe integer and throws when it is too large. + * + * @category unsafe + * @since 4.0.0 + */ +export const toNumberUnsafe = (self: ByteSize): number => + Option.getOrThrowWith(toNumber(self), () => new Error(`ByteSize exceeds Number.MAX_SAFE_INTEGER: ${self}`)) + +/** + * Converts a byte size to an approximate number of the specified unit. + * + * @category converting + * @since 4.0.0 + */ +export const toUnit: { + (unit: Unit): (self: ByteSize) => number + (self: ByteSize, unit: Unit): number +} = dual(2, (self: ByteSize, unit: Unit) => Number(self) / Number(unitsBySymbol.get(unit)!.factor)) + +/** + * Provides an order for byte sizes. + * + * @category instances + * @since 4.0.0 + */ +export const Order: order.Order = BI.Order + +/** + * Provides an equivalence for byte sizes. + * + * @category instances + * @since 4.0.0 + */ +export const Equivalence: Equ.Equivalence = BI.Equivalence + +/** + * Returns whether a byte size is in an inclusive range. + * + * @category predicates + * @since 4.0.0 + */ +export const between: { + (options: { minimum: ByteSize; maximum: ByteSize }): (self: ByteSize) => boolean + (self: ByteSize, options: { minimum: ByteSize; maximum: ByteSize }): boolean +} = BI.between + +/** + * Returns the smaller byte size. + * + * @category ordering + * @since 4.0.0 + */ +export const min: { + (that: ByteSize): (self: ByteSize) => ByteSize + (self: ByteSize, that: ByteSize): ByteSize +} = BI.min as any + +/** + * Returns the larger byte size. + * + * @category ordering + * @since 4.0.0 + */ +export const max: { + (that: ByteSize): (self: ByteSize) => ByteSize + (self: ByteSize, that: ByteSize): ByteSize +} = BI.max as any + +/** + * Constrains a byte size to an inclusive range. + * + * @category ordering + * @since 4.0.0 + */ +export const clamp: { + (options: { minimum: ByteSize; maximum: ByteSize }): (self: ByteSize) => ByteSize + (self: ByteSize, options: { minimum: ByteSize; maximum: ByteSize }): ByteSize +} = BI.clamp as any + +/** + * Checks whether the first byte size is less than the second. + * + * @category predicates + * @since 4.0.0 + */ +export const isLessThan: { + (that: ByteSize): (self: ByteSize) => boolean + (self: ByteSize, that: ByteSize): boolean +} = BI.isLessThan + +/** + * Checks whether the first byte size is at most the second. + * + * @category predicates + * @since 4.0.0 + */ +export const isLessThanOrEqualTo: { + (that: ByteSize): (self: ByteSize) => boolean + (self: ByteSize, that: ByteSize): boolean +} = BI.isLessThanOrEqualTo + +/** + * Checks whether the first byte size is greater than the second. + * + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThan: { + (that: ByteSize): (self: ByteSize) => boolean + (self: ByteSize, that: ByteSize): boolean +} = BI.isGreaterThan + +/** + * Checks whether the first byte size is at least the second. + * + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThanOrEqualTo: { + (that: ByteSize): (self: ByteSize) => boolean + (self: ByteSize, that: ByteSize): boolean +} = BI.isGreaterThanOrEqualTo + +/** + * Checks whether two byte sizes contain the same count. + * + * @category predicates + * @since 4.0.0 + */ +export const equals: { + (that: ByteSize): (self: ByteSize) => boolean + (self: ByteSize, that: ByteSize): boolean +} = dual(2, Equivalence) + +/** + * Adds two byte sizes exactly. + * + * @category math + * @since 4.0.0 + */ +export const sum: { + (that: ByteSize): (self: ByteSize) => ByteSize + (self: ByteSize, that: ByteSize): ByteSize +} = BI.sum as any + +/** + * Subtracts byte sizes, returning `None` on underflow. + * + * @category math + * @since 4.0.0 + */ +export const subtract: { + (that: ByteSize): (self: ByteSize) => Option.Option + (self: ByteSize, that: ByteSize): Option.Option +} = dual( + 2, + (self: ByteSize, that: ByteSize) => self < that ? Option.none() : Option.some(make(self - that)) +) + +/** + * Subtracts byte sizes and throws on underflow. + * + * @category unsafe + * @since 4.0.0 + */ +export const subtractUnsafe: { + (that: ByteSize): (self: ByteSize) => ByteSize + (self: ByteSize, that: ByteSize): ByteSize +} = dual(2, (self: ByteSize, that: ByteSize) => { + if (self < that) throw new Error(`ByteSize subtraction underflow: ${self} - ${that}`) + return make(self - that) +}) + +const scalar = (input: number | bigint, positive: boolean): bigint | undefined => { + if (typeof input === "bigint") return input >= (positive ? bigint1 : bigint0) ? input : undefined + return Number.isSafeInteger(input) && input >= (positive ? 1 : 0) ? BigInt(input) : undefined +} + +/** + * Multiplies a byte size by a non-negative integer scalar. + * + * @category math + * @since 4.0.0 + */ +export const times: { + (multiplier: number | bigint): (self: ByteSize) => Option.Option + (self: ByteSize, multiplier: number | bigint): Option.Option +} = dual(2, (self: ByteSize, multiplier: number | bigint) => { + const value = scalar(multiplier, false) + return value === undefined ? Option.none() : Option.some(make(self * value)) +}) + +/** + * Divides a byte size by a positive integer, discarding any remainder. + * + * @category math + * @since 4.0.0 + */ +export const divide: { + (divisor: number | bigint): (self: ByteSize) => Option.Option + (self: ByteSize, divisor: number | bigint): Option.Option +} = dual(2, (self: ByteSize, divisor: number | bigint) => { + const value = scalar(divisor, true) + return value === undefined ? Option.none() : Option.some(make(self / value)) +}) + +const validatePrecision = (precision: number): number => { + if (!Number.isSafeInteger(precision) || precision < 0 || precision > 20) { + throw new Error(`ByteSize format precision must be an integer from 0 to 20, received ${precision}`) + } + return precision +} + +const formatWithUnit = (value: bigint, unit: UnitInfo, precision: number, trailingZeros: boolean): string => { + const scale = BigInt(10) ** BigInt(precision) + const rounded = (value * scale * BigInt(2) + unit.factor) / (unit.factor * BigInt(2)) + const whole = rounded / scale + if (precision === 0) return `${whole} ${unit.symbol}` + let fraction = `${rounded % scale}`.padStart(precision, "0") + if (!trailingZeros) fraction = fraction.replace(/0+$/, "") + return `${whole}${fraction.length === 0 ? "" : `.${fraction}`} ${unit.symbol}` +} + +/** + * Formats a byte size with canonical decimal or binary unit symbols. + * + * @category converting + * @since 4.0.0 + */ +export const format = (self: ByteSize, options: FormatOptions = {}): string => { + const precision = validatePrecision(options.precision ?? 2) + const trailingZeros = options.trailingZeros ?? false + if (options.unit !== undefined) { + return formatWithUnit(self, unitsBySymbol.get(options.unit)!, precision, trailingZeros) + } + const units = options.system === "decimal" ? decimalUnits : binaryUnits + if (self === bigint0) return "0 B" + let index = units.length - 1 + while (index > 0 && self < units[index].factor) index-- + if (index < units.length - 1) { + const scale = BigInt(10) ** BigInt(precision) + const rounded = (self * scale * BigInt(2) + units[index].factor) / (units[index].factor * BigInt(2)) + const base = options.system === "decimal" ? decimalBase : binaryBase + if (rounded >= base * scale) index++ + } + return formatWithUnit(self, units[index], precision, trailingZeros) +} + +/** + * Reducer that sums byte sizes from zero. + * + * @category math + * @since 4.0.0 + */ +export const ReducerSum: Reducer.Reducer = BI.ReducerSum as any + +/** + * Combiner that keeps the largest byte size. + * + * @category math + * @since 4.0.0 + */ +export const CombinerMax: Combiner.Combiner = BI.CombinerMax as any + +/** + * Combiner that keeps the smallest byte size. + * + * @category math + * @since 4.0.0 + */ +export const CombinerMin: Combiner.Combiner = BI.CombinerMin as any diff --git a/repos/effect/packages/effect/src/Cache.ts b/repos/effect/packages/effect/src/Cache.ts index d1e3091b24..0bc335802d 100644 --- a/repos/effect/packages/effect/src/Cache.ts +++ b/repos/effect/packages/effect/src/Cache.ts @@ -447,7 +447,10 @@ export const get: { MutableHashMap.remove(self.map, key) } }) - MutableHashMap.set(self.map, key, entry) + const exit = entry.fiber.pollUnsafe() + if (exit === undefined || !effect.exitHasInterrupts(exit)) { + MutableHashMap.set(self.map, key, entry) + } if (Number.isFinite(self.capacity)) { checkCapacity(self) } @@ -1041,6 +1044,10 @@ export const invalidateWhen: { return oentry.await().pipe( effect.map((value) => { if (f(value)) { + const current = MutableHashMap.get(self.map, key) + if (Option.isNone(current) || current.value !== oentry) { + return false + } MutableHashMap.remove(self.map, key) return true } @@ -1165,12 +1172,18 @@ export const refresh: { } entry.fiber.addObserver((exit) => { if (effect.exitHasInterrupts(exit)) { - if (!existing) MutableHashMap.remove(self.map, key) + const current = MutableHashMap.get(self.map, key) + if (Option.isSome(current) && current.value === entry) { + MutableHashMap.remove(self.map, key) + } return } const ttl = self.timeToLive(exit, key) if (Duration.isZero(ttl)) { - MutableHashMap.remove(self.map, key) + const current = MutableHashMap.get(self.map, key) + if (existing || (Option.isSome(current) && current.value === entry)) { + MutableHashMap.remove(self.map, key) + } return effect.void } entry.expiresAt = Duration.isFinite(ttl) @@ -1178,6 +1191,7 @@ export const refresh: { : undefined if (existing) { MutableHashMap.set(self.map, key, entry) + checkCapacity(self) } }) return entry.await() diff --git a/repos/effect/packages/effect/src/Cause.ts b/repos/effect/packages/effect/src/Cause.ts index 2511f27bce..9cda574ab1 100644 --- a/repos/effect/packages/effect/src/Cause.ts +++ b/repos/effect/packages/effect/src/Cause.ts @@ -261,7 +261,7 @@ export declare namespace Cause { readonly [ReasonTypeId]: typeof ReasonTypeId readonly _tag: Tag readonly annotations: ReadonlyMap - annotate(annotations: Context.Context | ReadonlyMap, options?: { + annotate(annotations: Context.Context, options?: { readonly overwrite?: boolean | undefined }): this } diff --git a/repos/effect/packages/effect/src/Channel.ts b/repos/effect/packages/effect/src/Channel.ts index cbd1247629..930672fb31 100644 --- a/repos/effect/packages/effect/src/Channel.ts +++ b/repos/effect/packages/effect/src/Channel.ts @@ -20,6 +20,7 @@ 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 * as Count from "./internal/count.ts" import { ClockRef, endSpan, scopeFinalizerCountUnsafe } from "./internal/effect.ts" import { addSpanStackTrace } from "./internal/tracer.ts" import * as Iterable from "./Iterable.ts" @@ -708,6 +709,11 @@ export const fromChunk = (chunk: Chunk.Chunk): Channel => fromArray(Chu /** * Creates a `Channel` from an iterator that emits arrays of elements. * + * **Details** + * + * Finite fractional `chunkSize` values are rounded down. `NaN` and non-positive + * values are treated as `1` so every successful pull emits a non-empty array. + * * **Example** (Batching iterator output) * * ```ts import.meta.vitest @@ -754,15 +760,16 @@ export const fromChunk = (chunk: Chunk.Chunk): Channel => fromArray(Chu export const fromIteratorArray = ( iterator: LazyArg>, chunkSize = DefaultChunkSize -): Channel, never, L> => - fromPull( +): Channel, never, L> => { + const size = Count.normalizeNonEmpty(chunkSize) + return 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) { + while (buffer.length < size) { const state = iter.next() if (state.done) { if (buffer.length === 0) { @@ -777,6 +784,7 @@ export const fromIteratorArray = ( }) }) ) +} /** * Creates a `Channel` that emits all elements from an iterable. @@ -800,6 +808,11 @@ export const fromIterable = (iterable: Iterable): Channel Channel.succeed(`recovered: ${defect}`)) + * ) + * + * Effect.runSync(Channel.runCollect(channel)) // => ["recovered: boom"] + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const catchDefect: { + ( + f: (defect: unknown) => Channel + ): ( + self: Channel + ) => Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + f: (defect: unknown) => Channel + ): Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > +} = dual(2, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + f: (defect: unknown) => Channel +): Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 +> => catchCauseFilter(self, Cause.findDefect, f)) + /** * Runs an effect with the full failure `Cause` when the channel fails, then * fails the returned channel with the original cause. @@ -6502,9 +6606,8 @@ export const splitLines = (): Channel< // Accumulates text that has not yet been terminated by a line break. // Content is carried across chunks until a terminator is found. let stringBuilder = "" - // Set when a chunk ends with \r so the next chunk can check whether - // the following character is \n (completing a \r\n pair) or not - // (standalone \r, which is itself a line terminator). + // A trailing \r completes the line immediately. Remember it only to + // suppress a leading \n in the next nonempty string. let midCRLF = false // Remembers the upstream Done value after the first time the upstream // signals completion, so subsequent pulls return Done immediately @@ -6531,11 +6634,8 @@ export const splitLines = (): Channel< let indexOfLF = str.indexOf("\n") if (midCRLF) { if (indexOfLF === 0) { - pushLine("") from = 1 indexOfLF = str.indexOf("\n", from) - } else { - pushLine("") } midCRLF = false } @@ -6545,18 +6645,19 @@ export const splitLines = (): Channel< from = indexOfLF + 1 indexOfLF = str.indexOf("\n", from) } else { + pushLine(str.substring(from, indexOfCR)) if (str.length === indexOfCR + 1) { midCRLF = true + from = str.length indexOfCR = -1 } else { - pushLine(str.substring(from, indexOfCR)) from = indexOfCR + (indexOfLF === indexOfCR + 1 ? 2 : 1) indexOfCR = str.indexOf("\r", from) indexOfLF = str.indexOf("\n", from) } } } - stringBuilder = stringBuilder + str.substring(from, str.length - (midCRLF ? 1 : 0)) + stringBuilder = stringBuilder + str.substring(from) } } return Arr.isReadonlyArrayNonEmpty(chunkBuilder) ? chunkBuilder : null @@ -6571,7 +6672,7 @@ export const splitLines = (): Channel< onFailure: Effect.failCause, onDone: (leftover) => { done = Option.some(leftover) - if (stringBuilder.length > 0 || midCRLF) { + if (stringBuilder.length > 0) { const last = stringBuilder stringBuilder = "" midCRLF = false @@ -7968,6 +8069,11 @@ export const runForEachWhile: { /** * Concatenates a channel's `Uint8Array` chunks into a single `Uint8Array`. * + * **Gotchas** + * + * This materializes the full content in memory. The source channel must not + * reuse or mutate emitted buffers, which are retained until collection completes. + * * **Example** (Joining channel byte chunks) * * ```ts import.meta.vitest @@ -7982,11 +8088,6 @@ export const runForEachWhile: { * Array.from(bytes) // => [1, 2, 3, 4] * ``` * - * **Gotchas** - * - * This materializes the full content in memory. The source channel must not - * reuse or mutate emitted buffers, which are retained until collection completes. - * * @category running * @since 4.0.0 */ @@ -8055,16 +8156,6 @@ export const runCollect = ( return acc }) -/** - * Runs a channel and outputs the done value. - * - * @category running - * @since 4.0.0 - */ -export const runDone = ( - self: Channel -): Effect.Effect => runWith(self, identity_, Effect.succeed) - /** * Runs a channel until the first output element is available, returning it in * an `Option`. diff --git a/repos/effect/packages/effect/src/Chunk.ts b/repos/effect/packages/effect/src/Chunk.ts index 7a75730a68..1e6a6d94a8 100644 --- a/repos/effect/packages/effect/src/Chunk.ts +++ b/repos/effect/packages/effect/src/Chunk.ts @@ -19,6 +19,7 @@ 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 * as Count from "./internal/count.ts" import type { NonEmptyIterable } from "./NonEmptyIterable.ts" import type { Option } from "./Option.ts" import * as O from "./Option.ts" @@ -30,7 +31,7 @@ import * as R from "./Result.ts" import type { Result } from "./Result.ts" import type { Covariant, NoInfer } from "./Types.ts" -const TypeId = "~effect/collections/Chunk" +const TypeId = "~effect/Chunk" /** * A Chunk is an immutable, ordered collection optimized for efficient concatenation and access patterns. @@ -248,7 +249,7 @@ const makeChunk = (backing: Backing): Chunk => { } case "ISlice": { chunk.length = backing.length - chunk.depth = backing.chunk.depth + 1 + chunk.depth = 0 chunk.left = _empty chunk.right = _empty break @@ -712,6 +713,7 @@ export const prepend: { /** * Takes the first up to `n` elements from the chunk. + * `n` is rounded down, with `NaN` and non-positive values treated as `0`. * * **Example** (Taking elements from the start) * @@ -729,7 +731,7 @@ export const take: { (n: number): (self: Chunk) => Chunk (self: Chunk, n: number): Chunk } = dual(2, (self: Chunk, _n: number): Chunk => { - const n = Math.floor(_n) + const n = Count.normalize(_n) if (n <= 0) { return _empty } else if (n >= self.length) { @@ -769,6 +771,7 @@ export const take: { /** * Drops the first up to `n` elements from the chunk. + * `n` is rounded down, with `NaN` and non-positive values treated as `0`. * * **Example** (Dropping elements from the start) * @@ -786,7 +789,7 @@ export const drop: { (n: number): (self: Chunk) => Chunk (self: Chunk, n: number): Chunk } = dual(2, (self: Chunk, _n: number): Chunk => { - const n = Math.floor(_n) + const n = Count.normalize(_n) if (n <= 0) { return self } else if (n >= self.length) { @@ -825,6 +828,7 @@ export const drop: { /** * Drops the last `n` elements. + * `n` is rounded down, with `NaN` and non-positive values treated as `0`. * * **Example** (Dropping elements from the end) * @@ -841,7 +845,10 @@ export const drop: { 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))) +} = dual( + 2, + (self: Chunk, n: number): Chunk => take(self, self.length - Math.min(Count.normalize(n), self.length)) +) /** * Drops all elements so long as the predicate returns true. @@ -1231,12 +1238,13 @@ export const flatten: >>(self: S) => Chunk.Flatten * * **Details** * - * The final chunk may contain fewer than `n` elements. Empty input produces an - * empty chunk of chunks. + * `n` is rounded down and normalized to at least `1`. 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. + * `NaN` and values of `n` less than or equal to zero produce singleton chunks. * * **Example** (Splitting into fixed-size chunks) * @@ -1263,11 +1271,12 @@ export const chunksOf: { (n: number): (self: Chunk) => Chunk> (self: Chunk, n: number): Chunk> } = dual(2, (self: Chunk, n: number) => { + const size = Count.normalizeNonEmpty(n) const gr: Array> = [] let current: Array = [] toReadonlyArray(self).forEach((a) => { current.push(a) - if (current.length >= n) { + if (current.length >= size) { gr.push(fromArrayUnsafe(current)) current = [] } @@ -1918,9 +1927,9 @@ export const splitAt: { * * **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. + * `n` is rounded down and normalized to at least `1`, with `NaN` treated as + * `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) * @@ -1948,7 +1957,7 @@ 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)) + const _n = Count.normalizeNonEmpty(n) return _n >= self.length ? [self, empty()] : [take(self, _n), drop(self, _n)] @@ -1959,8 +1968,9 @@ export const splitNonEmptyAt: { * * **Details** * - * The chunk size is derived from the input length and `n`; the final chunk may - * contain fewer elements than the others. + * `n` is rounded down and normalized to at least `1`, with `NaN` treated as + * `1`. The chunk size is derived from the input length and normalized count; + * the final chunk may contain fewer elements than the others. * * **Example** (Splitting chunks into groups) * @@ -1987,7 +1997,7 @@ export const splitNonEmptyAt: { 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)))) +} = dual(2, (self: Chunk, n: number) => chunksOf(self, Math.ceil(self.length / Count.normalizeNonEmpty(n)))) /** * Splits this chunk on the first element that matches this predicate. @@ -2079,6 +2089,7 @@ export const tailNonEmpty = (self: NonEmptyChunk): Chunk => drop(self, /** * Takes the last `n` elements. + * `n` is rounded down, with `NaN` and non-positive values treated as `0`. * * **Example** (Taking elements from the end) * @@ -2101,7 +2112,10 @@ export const tailNonEmpty = (self: NonEmptyChunk): Chunk => drop(self, 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)) +} = dual( + 2, + (self: Chunk, n: number): Chunk => drop(self, self.length - Math.min(Count.normalize(n), self.length)) +) /** * Takes all elements so long as the predicate returns true. @@ -2405,7 +2419,8 @@ export const replace: { * * **Details** * - * `n` is normalized to an integer greater than or equal to `1`. + * `n` is rounded down and normalized to an integer greater than or equal to + * `1`. `NaN` is treated as `1`. * * **Example** (Generating chunks from indices) * diff --git a/repos/effect/packages/effect/src/Config.ts b/repos/effect/packages/effect/src/Config.ts index 9de373eb13..7f6cde1830 100644 --- a/repos/effect/packages/effect/src/Config.ts +++ b/repos/effect/packages/effect/src/Config.ts @@ -40,7 +40,7 @@ const TypeId = "~effect/Config" * ```ts import.meta.vitest * import { Config } from "effect" * - * Config.isConfig(Config.string("HOST")) // => true + * Config.isConfig(Config.String("HOST")) // => true * Config.isConfig("not a config") // => false * ``` * @@ -236,7 +236,7 @@ const preserveInputEvidence = ( * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const upper = Config.string("name").pipe( + * const upper = Config.String("name").pipe( * Config.map((s) => s.toUpperCase()) * ) * @@ -244,7 +244,7 @@ const preserveInputEvidence = ( * Effect.runSync(upper.parse(provider)) // => "ALICE" * ``` * - * @see {@link mapOrFail} – when the transformation can fail + * @see {@link mapEffect} – when the transformation can fail * * @category mapping * @since 2.0.0 @@ -274,8 +274,8 @@ export const map: { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const trimmed = Config.string("name").pipe( - * Config.mapOrFail((s) => Effect.succeed(s.trim())) + * const trimmed = Config.String("name").pipe( + * Config.mapEffect((s) => Effect.succeed(s.trim())) * ) * const provider = ConfigProvider.fromUnknown({ name: " Alice " }) * Effect.runSync(trimmed.parse(provider)) // => "Alice" @@ -286,7 +286,7 @@ export const map: { * @category mapping * @since 2.0.0 */ -export const mapOrFail: { +export const mapEffect: { (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 => { @@ -327,7 +327,7 @@ export const mapOrFail: { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const hostConfig = Config.string("HOST").pipe( + * const hostConfig = Config.String("HOST").pipe( * Config.orElse(() => Config.succeed("localhost")) * ) * const provider = ConfigProvider.fromUnknown({}) @@ -386,8 +386,8 @@ export const orElse: { * import { Config, ConfigProvider, Effect } from "effect" * * const dbConfig = Config.all({ - * host: Config.string("host"), - * port: Config.number("port") + * host: Config.String("host"), + * port: Config.Number("port") * }) * * const provider = ConfigProvider.fromUnknown({ host: "localhost", port: 5432 }) @@ -409,12 +409,12 @@ export function all> | Record { - const configs: Array> | Record> = Array.isArray(arg) + const configs: Array> | Record> = globalThis.Array.isArray(arg) ? arg : Symbol.iterator in arg ? [...arg as any] : arg - if (Array.isArray(configs)) { + if (globalThis.Array.isArray(configs)) { return make((provider, pathPrefix) => Effect.flatMapEager( Effect.all(configs.map((config) => Effect.result(evaluateAt(config, provider, pathPrefix)))), @@ -513,7 +513,7 @@ const resolveRecord = ( * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const port = Config.number("port").pipe(Config.withDefault(3000)) + * const port = Config.Number("port").pipe(Config.withDefault(3000)) * * const provider = ConfigProvider.fromUnknown({}) * Effect.runSync(port.parse(provider)) // => 3000 @@ -558,7 +558,7 @@ export const withDefault: { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect, Option } from "effect" * - * const maybePort = Config.option(Config.number("port")) + * const maybePort = Config.option(Config.Number("port")) * * const provider = ConfigProvider.fromUnknown({}) * Effect.runSync(maybePort.parse(provider)) // => Option.none() @@ -641,7 +641,7 @@ type IsPlainObject = [A] extends [Record] * const makeConfig = (config: Config.Wrap): Config.Config => * Config.unwrap(config) * - * const config = makeConfig({ key: Config.string("key") }) + * const config = makeConfig({ key: Config.String("key") }) * const provider = ConfigProvider.fromUnknown({ key: "value" }) * Effect.runSync(config.parse(provider)) // => { key: "value" } * ``` @@ -807,7 +807,7 @@ const toConfigCursorAST = memoize((root: SchemaAST.AST): SchemaAST.AST => { * {@link nested} calls. Pass a single string for a flat key or an array for * nested paths. * - * Convenience constructors such as `string`, `number`, and `boolean` delegate + * Convenience constructors such as `String`, `Number`, and `Boolean` delegate * to this API. * * The codec is converted to its canonical `StringTree` form. Its encoded shape @@ -868,7 +868,7 @@ const toConfigCursorAST = memoize((root: SchemaAST.AST): SchemaAST.AST => { * Effect.runSync(DbConfig.parse(provider)) // => { host: "localhost", port: 5432 } * ``` * - * @see {@link string} / {@link number} / {@link boolean} – shortcuts for + * @see {@link String} / {@link Number} / {@link Boolean} – shortcuts for * single-value configs * * @category schemas @@ -905,172 +905,20 @@ export function schema(codec: Schema.ConstraintCodec, path?: stri }) } -/** @internal */ -export const TrueValues = Schema.Literals(["true", "yes", "on", "1", "y"]) +const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })) -/** @internal */ -export const FalseValues = Schema.Literals(["false", "no", "off", "0", "n"]) +const LogLevelSchema = Schema.Literals(LogLevel_.values) -/** - * Schema for boolean values encoded as strings. - * - * **When to use** - * - * Use when you need the reusable boolean schema value for `Config.schema` with - * custom paths. - * - * **Details** - * - * Accepted string values: `true`, `false`, `yes`, `no`, `on`, `off`, `1`, - * `0`, `y`, `n` (case-sensitive). - * - * @see {@link boolean} – convenience constructor - * - * @category schemas - * @since 4.0.0 - */ -export const Boolean = Schema.Literals([...TrueValues.literals, ...FalseValues.literals]).pipe( - Schema.decodeTo( - Schema.Boolean, - SchemaTransformation.transform({ - decode: (value) => value === "true" || value === "yes" || value === "on" || value === "1" || value === "y", - encode: (value) => value ? "true" : "false" - }) - ) -) - -/** - * Schema for port numbers (integers in 1–65535). - * - * **When to use** - * - * Use when you need the reusable port schema value for `Config.schema` with - * custom paths. - * - * @see {@link port} – convenience constructor - * - * @category schemas - * @since 4.0.0 - */ -export const Port = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })) - -/** - * Schema for `LogLevel` string literals. - * - * **When to use** - * - * Use when you need the reusable log-level schema value for `Config.schema` - * with custom paths. - * - * **Details** - * - * Accepted values: `"All"`, `"Fatal"`, `"Error"`, `"Warn"`, `"Info"`, - * `"Debug"`, `"Trace"`, `"None"`. - * - * @see {@link logLevel} – convenience constructor - * - * @category schemas - * @since 4.0.0 - */ -export const LogLevel = Schema.Literals(LogLevel_.values) - -/** - * Schema for key-value record types that can also be parsed from - * a flat comma-separated string. - * - * **When to use** - * - * Use when reading key-value maps from a single env var (e.g. OpenTelemetry - * resource attributes). - * - * **Details** - * - * Accepts either a JSON-like record from the provider or a flat string like - * `"key1=val1,key2=val2"`. The `separator` (default `","`) and - * `keyValueSeparator` (default `"="`) can be customized. - * - * **Example** (Parsing a comma-separated record) - * - * ```ts import.meta.vitest - * 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" - * } - * }) - * - * const result = Effect.runSync(config.parse(provider)) - * result["service.name"] // => "my-service" - * result["service.version"] // => "1.0.0" - * result["custom.attribute"] // => "value" - * ``` - * - * @see {@link Array} for separated or structural array input - * - * @category schemas - * @since 4.0.0 - */ -export const Record = (key: K, value: V, options?: { +interface ArrayOptions { readonly separator?: string | undefined - readonly keyValueSeparator?: string | undefined -}) => { - const record = Schema.Record(key, value) - const split = SchemaTransformation.splitKeyValue(options) - const recordString = Schema.String.pipe( - Schema.decodeTo(Schema.toCodecStringTree(record), { - decode: split.decode, - encode: SchemaGetter.passthrough, Schema.StringTree>({ strict: false }).compose( - split.encode - ) - }) - ) - - return Schema.Union([record, recordString]) } -const ArrayConfig = (value: V, options?: { +interface RecordOptions { readonly separator?: string | undefined -}) => { - const array = Schema.Array(value) - const separator = options?.separator ?? "," - const arrayString = Schema.String.pipe( - Schema.decodeTo(Schema.toCodecStringTree(array), { - decode: SchemaGetter.split(options), - encode: SchemaGetter.passthrough, Schema.StringTree>({ strict: false }).compose( - SchemaGetter.transform((input) => input.join(separator)) - ) - }) - ) - - return Schema.Union([arrayString, array]) + readonly keyValueSeparator?: string | undefined } -export { - /** - * Schema for array types that can also be parsed from a flat separated string. - * - * **When to use** - * - * Use when reading array values from a single env var, such as comma-separated - * exporter names. - * - * **Details** - * - * Accepts either a JSON-like array from the provider or a flat string like - * `"a,b,c"`. The `separator` defaults to `","` and can be customized. - * - * @see {@link Record} for separated or structural record input - * - * @category schemas - * @since 4.0.0 - */ - ArrayConfig as Array -} +const isPath = (u: unknown): u is string | Path => Predicate.isString(u) || globalThis.Array.isArray(u) // ----------------------------------------------------------------------------- // constructors @@ -1105,7 +953,7 @@ export function fail(err: SourceError | Schema.SchemaError) { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const host = Config.string("HOST").pipe( + * const host = Config.String("HOST").pipe( * Config.orElse(() => Config.succeed("localhost")) * ) * const provider = ConfigProvider.fromUnknown({}) @@ -1135,19 +983,19 @@ export function succeed(value: T) { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const host = Config.string("HOST") + * 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 NonEmptyString} – rejects empty strings * @see {@link schema} – for more complex types * * @category constructors * @since 2.0.0 */ -export function string(name?: string) { +export function String(name?: string) { return schema(Schema.String, name) } @@ -1163,12 +1011,12 @@ export function string(name?: string) { * * Shortcut for `Config.schema(Schema.NonEmptyString, name)`. * - * @see {@link string} for allowing empty strings + * @see {@link String} for allowing empty strings * * @category constructors * @since 3.7.0 */ -export function nonEmptyString(name?: string) { +export function NonEmptyString(name?: string) { return schema(Schema.NonEmptyString, name) } @@ -1184,13 +1032,13 @@ export function nonEmptyString(name?: string) { * * Shortcut for `Config.schema(Schema.Number, name)`. * - * @see {@link finite} for rejecting `NaN` and `Infinity` - * @see {@link int} for accepting only integers + * @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) { +export function Number(name?: string) { return schema(Schema.Number, name) } @@ -1205,13 +1053,13 @@ export function number(name?: string) { * * Shortcut for `Config.schema(Schema.Finite, name)`. * - * @see {@link number} for accepting `NaN` and `Infinity` - * @see {@link int} for accepting only integers + * @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) { +export function Finite(name?: string) { return schema(Schema.Finite, name) } @@ -1226,13 +1074,13 @@ export function finite(name?: string) { * * 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` + * @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) { +export function Int(name?: string) { return schema(Schema.Int, name) } @@ -1252,16 +1100,16 @@ export function int(name?: string) { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const env = Config.literal("production", "ENV") + * const env = Config.Literal("production", "ENV") * const provider = ConfigProvider.fromUnknown({ ENV: "production" }) * Effect.runSync(env.parse(provider)) // => "production" * ``` * - * @see {@link literals} – accepts multiple literal values + * @see {@link Literals} – accepts multiple literal values * @category constructors * @since 2.0.0 */ -export function literal(literal: L, name?: string) { +export function Literal(literal: L, name?: string) { return schema(Schema.Literal(literal), name) } @@ -1281,20 +1129,155 @@ export function literal(literal: L, name?: str * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const env = Config.literals(["development", "production"], "ENV") + * const env = Config.Literals(["development", "production"], "ENV") * const provider = ConfigProvider.fromUnknown({ ENV: "development" }) * Effect.runSync(env.parse(provider)) // => "development" * ``` * - * @see {@link literal} for accepting one specific literal value + * @see {@link Literal} for accepting one specific literal value * * @category constructors * @since 4.0.0 */ -export function literals>(literals: L, name?: string) { +export function Literals>(literals: L, name?: string) { return schema(Schema.Literals(literals), name) } +/** + * Creates a config for array values that may also be read from a separated string. + * + * **When to use** + * + * Use when you need to read either structural array input or a flat value such as + * `"otlp,console"` from an environment variable. + * + * **Details** + * + * Pass a string or `ConfigProvider.Path` as the second argument to set the lookup + * path. When no path is needed, pass the options object directly. The `separator` + * defaults to `","`. + * + * **Example** (Reading a comma-separated array) + * + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Effect, Schema } from "effect" + * + * const config = Config.Array(Schema.String, "EXPORTERS") + * const provider = ConfigProvider.fromEnv({ env: { EXPORTERS: "otlp,console" } }) + * + * Effect.runSync(config.parse(provider)) // => ["otlp", "console"] + * ``` + * + * @see {@link Record} for key-value input from structural records or separated strings. + * @category constructors + * @since 4.0.0 + */ +export function Array>( + value: V, + options?: ArrayOptions +): Config> +export function Array>( + value: V, + path: string | Path, + options?: ArrayOptions +): Config> +export function Array>( + value: V, + pathOrOptions?: string | Path | ArrayOptions, + options?: ArrayOptions +) { + const hasPath = isPath(pathOrOptions) + const resolvedOptions = hasPath ? options : pathOrOptions + const array = Schema.Array(value) + const separator = resolvedOptions?.separator ?? "," + const arrayString = Schema.String.pipe( + Schema.decodeTo(Schema.toCodecStringTree(array), { + decode: SchemaGetter.split(resolvedOptions), + encode: SchemaGetter.passthrough, Schema.StringTree>({ strict: false }).compose( + SchemaGetter.transform((input) => input.join(separator)) + ) + }) + ) + return schema(Schema.Union([arrayString, array]), hasPath ? pathOrOptions : undefined) +} + +/** + * Creates a config for record values that may also be read from a separated key-value string. + * + * **When to use** + * + * Use when you need to read either structural record input or a flat value such as + * `"service.name=my-service,service.version=1.0.0"` from an environment variable. + * + * **Details** + * + * Pass a string or `ConfigProvider.Path` as the third argument to set the lookup + * path. When no path is needed, pass the options object directly. The `separator` + * defaults to `","` and `keyValueSeparator` defaults to `"="`. + * + * **Example** (Reading a comma-separated record) + * + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Effect, Schema } from "effect" + * + * const config = Config.Record(Schema.String, Schema.String, "OTEL_RESOURCE_ATTRIBUTES") + * const provider = ConfigProvider.fromEnv({ + * env: { + * OTEL_RESOURCE_ATTRIBUTES: + * "service.name=my-service,service.version=1.0.0,custom.attribute=value" + * } + * }) + * + * const result = Effect.runSync(config.parse(provider)) + * result["service.name"] // => "my-service" + * result["service.version"] // => "1.0.0" + * result["custom.attribute"] // => "value" + * ``` + * + * @see {@link Array} for array input from structural arrays or separated strings. + * @category constructors + * @since 4.0.0 + */ +export function Record< + K extends Schema.Record.Key & Schema.ConstraintCodec, + V extends Schema.ConstraintCodec +>( + key: K, + value: V, + options?: RecordOptions +): Config> +export function Record< + K extends Schema.Record.Key & Schema.ConstraintCodec, + V extends Schema.ConstraintCodec +>( + key: K, + value: V, + path: string | Path, + options?: RecordOptions +): Config> +export function Record< + K extends Schema.Record.Key & Schema.ConstraintCodec, + V extends Schema.ConstraintCodec +>( + key: K, + value: V, + pathOrOptions?: string | Path | RecordOptions, + options?: RecordOptions +) { + const hasPath = isPath(pathOrOptions) + const record = Schema.Record(key, value) + const split = SchemaTransformation.splitKeyValue(hasPath ? options : pathOrOptions) + const recordString = Schema.String.pipe( + Schema.decodeTo(Schema.toCodecStringTree(record), { + decode: split.decode, + encode: SchemaGetter.passthrough, Schema.StringTree>({ strict: false }).compose( + split.encode + ) + }) + ) + return schema(Schema.Union([record, recordString]), hasPath ? pathOrOptions : undefined) +} + /** * Creates a config for a boolean value parsed from common string * representations. @@ -1305,8 +1288,6 @@ export function literals>( * * **Details** * - * Shortcut for `Config.schema(Config.Boolean, name)`. - * * Accepted values: `true`, `false`, `yes`, `no`, `on`, `off`, `1`, `0`, * `y`, `n`. * @@ -1315,7 +1296,7 @@ export function literals>( * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const program = Config.boolean("FEATURE_FLAG") + * const program = Config.Boolean("FEATURE_FLAG") * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1328,13 +1309,11 @@ export function literals>( * ) // => true * ``` * - * @see {@link Boolean} for the underlying boolean codec - * * @category constructors * @since 2.0.0 */ -export function boolean(name?: string) { - return schema(Boolean, name) +export function Boolean(name?: string) { + return schema(Schema.BooleanLiterals, name) } /** @@ -1357,7 +1336,7 @@ export function boolean(name?: string) { * ```ts import.meta.vitest * import { Config, ConfigProvider, Duration, Effect } from "effect" * - * const program = Config.duration("DURATION").pipe(Effect.map(Duration.toMillis)) + * const program = Config.Duration("DURATION").pipe(Effect.map(Duration.toMillis)) * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1375,10 +1354,25 @@ export function boolean(name?: string) { * @category constructors * @since 2.5.0 */ -export function duration(name?: string) { +export function Duration(name?: string) { return schema(Schema.DurationFromString, name) } +/** + * Creates a config for an exact, human-readable byte-size value. + * + * **Details** + * + * Decimal symbols such as `kB` use powers of 1,000, while binary symbols such + * as `KiB` use powers of 1,024. + * + * @category constructors + * @since 4.0.0 + */ +export function ByteSize(name?: string) { + return schema(Schema.ByteSize, name) +} + /** * Creates a config for a port number (integer in 1–65535). * @@ -1388,14 +1382,14 @@ export function duration(name?: string) { * * **Details** * - * Shortcut for `Config.schema(Config.Port, name)`. + * Accepts integers from `1` through `65535`. * * **Example** (Reading a port) * * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const program = Config.port("PORT") + * const program = Config.Port("PORT") * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1408,14 +1402,13 @@ export function duration(name?: string) { * ) // => 8080 * ``` * - * @see {@link int} for integer config values outside the port range - * @see {@link Port} for the underlying port codec + * @see {@link Int} for integer config values outside the port range * * @category constructors * @since 3.16.0 */ -export function port(name?: string) { - return schema(Port, name) +export function Port(name?: string) { + return schema(PortSchema, name) } /** @@ -1427,8 +1420,6 @@ export function port(name?: string) { * * **Details** * - * Shortcut for `Config.schema(Config.LogLevel, name)`. - * * Accepted values: `"All"`, `"Fatal"`, `"Error"`, `"Warn"`, `"Info"`, * `"Debug"`, `"Trace"`, `"None"`. * @@ -1437,7 +1428,7 @@ export function port(name?: string) { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const program = Config.logLevel("LOG_LEVEL") + * const program = Config.LogLevel("LOG_LEVEL") * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1450,13 +1441,11 @@ export function port(name?: string) { * ) // => "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) +export function LogLevel(name?: string) { + return schema(LogLevelSchema, name) } /** @@ -1477,7 +1466,7 @@ export function logLevel(name?: string) { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const program = Config.redacted("API_KEY").pipe(Effect.map(String)) + * const program = Config.Redacted("API_KEY").pipe(Effect.map(String)) * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1490,12 +1479,12 @@ export function logLevel(name?: string) { * ) // => "" * ``` * - * @see {@link string} for non-secret string settings + * @see {@link String} for non-secret string settings * * @category constructors * @since 2.0.0 */ -export function redacted(name?: string) { +export function Redacted(name?: string) { return schema(Schema.Redacted(Schema.String), name) } @@ -1519,7 +1508,7 @@ export function redacted(name?: string) { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const program = Config.url("URL").pipe(Effect.map((url) => url.href)) + * const program = Config.URL("URL").pipe(Effect.map((url) => url.href)) * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1537,7 +1526,7 @@ export function redacted(name?: string) { * @category constructors * @since 3.11.0 */ -export function url(name?: string) { +export function URL(name?: string) { return schema(Schema.URL, name) } @@ -1561,7 +1550,7 @@ export function url(name?: string) { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const createdAt = Config.date("CREATED_AT") + * const createdAt = Config.Date("CREATED_AT") * * const provider = ConfigProvider.fromUnknown({ CREATED_AT: "2024-01-15" }) * Effect.runSync(createdAt.parse(provider)).toISOString() // => "2024-01-15T00:00:00.000Z" @@ -1570,7 +1559,7 @@ export function url(name?: string) { * @category constructors * @since 2.0.0 */ -export function date(name?: string) { +export function Date(name?: string) { return schema(Schema.Date, name) } @@ -1596,8 +1585,8 @@ export function date(name?: string) { * import { Config, ConfigProvider, Effect } from "effect" * * const dbConfig = Config.all({ - * host: Config.string("host"), - * port: Config.number("port") + * host: Config.String("host"), + * port: Config.Number("port") * }).pipe(Config.nested("database")) * * const provider = ConfigProvider.fromUnknown({ @@ -1611,7 +1600,7 @@ export function date(name?: string) { * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const host = Config.string("host").pipe(Config.nested("database")) + * const host = Config.String("host").pipe(Config.nested("database")) * * const provider = ConfigProvider.fromEnv({ * env: { database_host: "localhost" } diff --git a/repos/effect/packages/effect/src/ConfigProvider.ts b/repos/effect/packages/effect/src/ConfigProvider.ts index a847dc7438..8dd6622c39 100644 --- a/repos/effect/packages/effect/src/ConfigProvider.ts +++ b/repos/effect/packages/effect/src/ConfigProvider.ts @@ -9,6 +9,7 @@ * @since 4.0.0 */ +import * as Arr from "./Array.ts" import * as Context from "./Context.ts" import * as Data from "./Data.ts" import * as Effect from "./Effect.ts" @@ -651,7 +652,7 @@ export const nested: { * ) * * const program = Effect.gen(function*() { - * const port = yield* Config.number("port") + * const port = yield* Config.Number("port") * return port * }) * @@ -697,7 +698,7 @@ export const layer = ( * // The current env provider is tried first; `defaults` is the fallback * const DefaultsLayer = ConfigProvider.layerAdd(defaults) * const BaseLayer = ConfigProvider.layer(ConfigProvider.fromUnknown({})) - * const program = Config.string("HOST") + * const program = Config.String("HOST") * * const layer = Layer.provide(DefaultsLayer, BaseLayer) * Effect.runSync(Effect.provide(program, layer)) // => "localhost" @@ -763,7 +764,7 @@ export const layerAdd = ( * } * }) * - * const host = Config.string("host").parse( + * const host = Config.String("host").parse( * provider.pipe(ConfigProvider.nested("database")) * ) * @@ -908,7 +909,7 @@ export function fromEnvRecord( * } * }) * - * const host = Config.string("HOST").parse( + * const host = Config.String("HOST").parse( * provider.pipe(ConfigProvider.nested("DATABASE")) * ) * @@ -958,8 +959,6 @@ function buildEnvTrie(env: Record): EnvTrieNode { return trie } -const NUMERIC_INDEX = /^(0|[1-9][0-9]*)$/ - function nodeAtEnv( trie: EnvTrieNode, env: Record, @@ -976,7 +975,7 @@ function nodeAtEnv( return leafValue === undefined ? undefined : makeValue(leafValue) } - const allNumeric = children.every((k) => NUMERIC_INDEX.test(k)) + const allNumeric = children.every(Arr.isCanonicalArrayIndex) if (allNumeric) { const length = Math.max(...children.map((k) => parseInt(k, 10))) + 1 return makeArray(length, leafValue) @@ -1136,7 +1135,7 @@ function interpolate(envValue: string, parsed: Record): string { : defaultValue ?? "" return interpolate( - envValue.replace(group, value), + envValue.replace(group, () => value), parsed ) } diff --git a/repos/effect/packages/effect/src/Context.ts b/repos/effect/packages/effect/src/Context.ts index 3db89c975c..52ee4d14c1 100644 --- a/repos/effect/packages/effect/src/Context.ts +++ b/repos/effect/packages/effect/src/Context.ts @@ -1031,6 +1031,7 @@ export const getUnsafe: { * @since 2.0.0 */ export const get: { + (service: Key): (self: Context) => S (service: Key): (self: Context) => S (self: Context, service: Key): S } = getUnsafe diff --git a/repos/effect/packages/effect/src/Cron.ts b/repos/effect/packages/effect/src/Cron.ts index 259c6a6f29..9d921ce567 100644 --- a/repos/effect/packages/effect/src/Cron.ts +++ b/repos/effect/packages/effect/src/Cron.ts @@ -25,7 +25,7 @@ import * as Result from "./Result.ts" import * as String from "./String.ts" import type { Mutable } from "./Types.ts" -const TypeId = "~effect/time/Cron" +const TypeId = "~effect/Cron" /** * Represents a cron schedule with time constraints and timezone information. @@ -435,7 +435,7 @@ const lookup = { } } -const CronParseErrorTypeId = "~effect/time/Cron/CronParseError" +const CronParseErrorTypeId = "~effect/Cron/CronParseError" /** * Represents an error that occurs when parsing a cron expression fails. diff --git a/repos/effect/packages/effect/src/Crypto.ts b/repos/effect/packages/effect/src/Crypto.ts index 9dfc905902..8a332c44b8 100644 --- a/repos/effect/packages/effect/src/Crypto.ts +++ b/repos/effect/packages/effect/src/Crypto.ts @@ -11,10 +11,11 @@ */ import * as Context from "./Context.ts" import * as Effect from "./Effect.ts" +import * as random from "./internal/random.ts" import * as Uuid from "./internal/uuid.ts" import * as PlatformError from "./PlatformError.ts" -const TypeId = "~effect/platform/Crypto" +const TypeId = "~effect/Crypto" /** * Digest algorithms supported by the platform `Crypto` service. @@ -251,7 +252,7 @@ export const make = ( random: Effect.sync(() => nextDoubleUnsafe()), randomBoolean: Effect.sync(() => nextDoubleUnsafe() > 0.5), randomInt: Effect.sync(() => nextIntUnsafe()), - randomBetween: (min, max) => Effect.sync(() => nextDoubleUnsafe() * (max - min) + min), + randomBetween: (min, max) => Effect.sync(() => random.nextBetween(min, max, nextDoubleUnsafe())), randomIntBetween(min, max, options) { const extra = options?.halfOpen === true ? 0 : 1 return Effect.sync(() => { diff --git a/repos/effect/packages/effect/src/Deferred.ts b/repos/effect/packages/effect/src/Deferred.ts index afe2ab3a97..14e75b9640 100644 --- a/repos/effect/packages/effect/src/Deferred.ts +++ b/repos/effect/packages/effect/src/Deferred.ts @@ -117,6 +117,12 @@ const DeferredProto = { } } +const DeferredImpl = function(this: any) { + this.resumes = undefined + this.effect = undefined +} as unknown as { new(): Deferred; prototype: any } +DeferredImpl.prototype = DeferredProto + /** * Creates an empty `Deferred` synchronously outside the `Effect` runtime. * @@ -137,12 +143,7 @@ const DeferredProto = { * @category unsafe * @since 4.0.0 */ -export const makeUnsafe = (): Deferred => { - const self = Object.create(DeferredProto) - self.resumes = undefined - self.effect = undefined - return self -} +export const makeUnsafe = (): Deferred => new DeferredImpl() /** * Creates a new `Deferred`. diff --git a/repos/effect/packages/effect/src/Duration.ts b/repos/effect/packages/effect/src/Duration.ts index 4f4e7fa1eb..2ffd929932 100644 --- a/repos/effect/packages/effect/src/Duration.ts +++ b/repos/effect/packages/effect/src/Duration.ts @@ -23,7 +23,7 @@ import { pipeArguments } from "./Pipeable.ts" import { hasProperty, isNumber } from "./Predicate.ts" import * as Reducer from "./Reducer.ts" -const TypeId = "~effect/time/Duration" +const TypeId = "~effect/Duration" const bigint0 = BigInt(0) const bigint1 = BigInt(1) diff --git a/repos/effect/packages/effect/src/Effect.ts b/repos/effect/packages/effect/src/Effect.ts index 3afd6618b1..b3139bfe93 100644 --- a/repos/effect/packages/effect/src/Effect.ts +++ b/repos/effect/packages/effect/src/Effect.ts @@ -40,7 +40,7 @@ import { CurrentLogAnnotations, CurrentLogSpans } from "./References.ts" import type * as Request from "./Request.ts" import type { RequestResolver } from "./RequestResolver.ts" import type * as Result from "./Result.ts" -import type { Schedule } from "./Schedule.ts" +import type { Metadata as ScheduleMetadata, Schedule } from "./Schedule.ts" import type { Scheduler } from "./Scheduler.ts" import type { Scope } from "./Scope.ts" import type { @@ -331,13 +331,8 @@ export declare namespace All { ] ? Mode extends true ? Result.Result<_A, _E> : _A : never }, - Mode extends true ? never - : keyof T extends never ? never - : T[keyof T] extends Effect ? _E - : never, - keyof T extends never ? never - : T[keyof T] extends Effect ? _R - : never + Mode extends true ? never : Error>, + Services> > : never @@ -377,6 +372,8 @@ export declare namespace All { : [Arg] extends [Iterable] ? ReturnIterable, IsResult> : [Arg] extends [Record] ? ReturnObject, IsResult> : never + + type ObjectValues = T extends unknown ? T[keyof T] : never } /** @@ -966,11 +963,12 @@ export const promise: ( * @category constructors * @since 2.0.0 */ -export const tryPromise: ( - options: - | { readonly try: (signal: AbortSignal) => PromiseLike; readonly catch: (error: unknown) => E } - | ((signal: AbortSignal) => PromiseLike) -) => Effect = internal.tryPromise +export const tryPromise: { + (options: (signal: AbortSignal) => PromiseLike): Effect + ( + options: { readonly try: (signal: AbortSignal) => PromiseLike; readonly catch: (error: unknown) => E } + ): Effect +} = internal.tryPromise /** * Creates an `Effect` that always succeeds with a given value. @@ -1635,12 +1633,13 @@ export const failCauseSync: ( */ export const die: (defect: unknown) => Effect = internal.die -const try_: ( - options: { +const try_: { + (options: LazyArg): Effect + (options: { readonly try: LazyArg readonly catch: (error: unknown) => E - } | LazyArg -) => Effect = internal.try + }): Effect +} = internal.try export { /** @@ -1777,6 +1776,26 @@ export const withFiber: ( evaluate: (fiber: Fiber) => Effect ) => Effect = core.withFiber +/** + * Accesses the current fiber to compute a successful value. + * + * **Example** (Computing a value from the current fiber) + * + * ```ts import.meta.vitest + * import { Effect } from "effect" + * + * const program = Effect.withFiberSucceed((fiber) => typeof fiber.id) + * + * Effect.runSync(program) // => "number" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const withFiberSucceed: ( + evaluate: (fiber: Fiber) => A +) => Effect = core.withFiberSucceed + // ----------------------------------------------------------------------------- // Conversions // ----------------------------------------------------------------------------- @@ -3922,7 +3941,7 @@ export const tapCauseFilter: { * @since 2.0.0 */ export const tapDefect: { - (f: (defect: unknown) => Effect): (self: Effect) => Effect + (f: (defect: unknown) => Effect): (self: Effect) => Effect (self: Effect, f: (defect: unknown) => Effect): Effect } = internal.tapDefect @@ -4399,8 +4418,8 @@ export const withErrorReporting: < >( effectOrOptions: Arg, options?: { readonly defectsOnly?: boolean | undefined } | undefined -) => [Arg] extends [Effect] ? Arg : (self: Effect) => Effect = - internal.withErrorReporting +) => [Arg] extends [Effect] ? Effect + : (self: Effect) => Effect = internal.withErrorReporting // ----------------------------------------------------------------------------- // Fallback @@ -4589,21 +4608,8 @@ export const timeoutOption: { } = internal.timeoutOption /** - * Applies a timeout to an effect, with a fallback effect executed if the timeout is reached. - * - * **When to use** - * - * Use when a timeout of an `Effect` should switch to a fallback effect. - * - * **Details** - * - * The fallback effect is created lazily by `orElse` and may introduce its own - * success, failure, and requirement types. - * - * **Gotchas** - * - * If the timeout wins, the source effect is interrupted before the fallback is - * run. + * Applies a timeout to an effect, lazily evaluating `orElse` after interrupting + * the source if the timeout is reached. * * **Example** (Falling back on timeout) * @@ -6948,6 +6954,8 @@ export const onExitPrimitive: ( * Ensures that a cleanup function runs whether this effect succeeds, fails, or * is interrupted. * + * **Details** + * * If both the effect and the cleanup function fail, the two causes are merged. * * **Example** (Observing every exit) @@ -7659,10 +7667,10 @@ export const repeat: { * const program = Effect.repeatOrElse( * task, * Schedule.recurs(3), - * (error, attempts) => + * (error, previous) => * Effect.sync(() => { output.push( * `Final failure: ${error}, after ${ - * Option.getOrElse(attempts, () => 0) + * Option.isSome(previous) ? previous.value.attempt : 0 * } attempts` * ) }).pipe(Effect.map(() => 0)) * ) @@ -7677,12 +7685,12 @@ export const repeat: { export const repeatOrElse: { ( schedule: Schedule, - orElse: (error: E | E2, option: Option) => Effect + orElse: (error: E | E2, option: Option>) => Effect ): (self: Effect) => Effect ( self: Effect, schedule: Schedule, - orElse: (error: E | E2, option: Option) => Effect + orElse: (error: E | E2, option: Option>) => Effect ): Effect } = internalSchedule.repeatOrElse @@ -14076,14 +14084,10 @@ export const withLogSpan = dual< // ----------------------------------------------------------------------------- /** - * Updates the `Metric` every time the `Effect` is executed. - * - * **Details** + * Updates a metric after each effect execution, optionally mapping its `Exit` to + * the metric's input. * - * Also accepts an optional function which can be used to map the `Exit` value - * of the `Effect` into a valid `Input` for the `Metric`. - * - * **Example** (Incrementing a metric for each execution) + * **Example** (Counting executions) * * ```ts import.meta.vitest * import { Effect, Metric } from "effect" @@ -14100,12 +14104,11 @@ export const withLogSpan = dual< * Effect.runSync(Metric.value(counter)).count // => 1 * ``` * - * **Example** (Mapping exits before updating a metric) + * **Example** (Mapping exits) * * ```ts import.meta.vitest * import { Effect, Exit, Metric } from "effect" * - * // Track different exit types with custom mapping * const exitTracker = Metric.frequency("exit_types", { * description: "Tracks success/failure/defect counts" * }) @@ -14130,7 +14133,7 @@ export const track: { ( metric: Metric.Metric, f: (exit: Exit.Exit) => Input - ): (self: Effect) => Effect + ): (self: Effect) => Effect ( metric: Metric.Metric, NoInfer>, State> ): (self: Effect) => Effect @@ -14937,6 +14940,9 @@ export declare namespace Effectify { : never } +type EffectifyArgs) => any> = Parameters extends [...infer Args, any] ? Args + : Parameters + /** * Converts an error-first callback API into a function that returns an * `Effect`. @@ -14993,12 +14999,12 @@ export const effectify: { ) => any>(fn: F): Effectify.Effectify> ) => any, E>( fn: F, - onError: (error: Effectify.EffectifyError, args: Parameters) => E + onError: (error: Effectify.EffectifyError, args: EffectifyArgs) => E ): Effectify.Effectify ) => any, E, E2>( fn: F, - onError: (error: Effectify.EffectifyError, args: Parameters) => E, - onSyncError: (error: unknown, args: Parameters) => E2 + onError: (error: Effectify.EffectifyError, args: EffectifyArgs) => E, + onSyncError: (error: unknown, args: EffectifyArgs) => E2 ): Effectify.Effectify } = ((fn: Function, onError?: (e: any, args: any) => any, onSyncError?: (e: any, args: any) => any) => diff --git a/repos/effect/packages/effect/src/Effectable.ts b/repos/effect/packages/effect/src/Effectable.ts index f4a114ad8d..7a1ca626f4 100644 --- a/repos/effect/packages/effect/src/Effectable.ts +++ b/repos/effect/packages/effect/src/Effectable.ts @@ -1,6 +1,6 @@ /** * Low-level helpers for making custom values behave like Effects. The module - * exposes a prototype builder and an abstract base class that let + * exposes a prototype builder, an abstract base class, and a mixin that let * domain-specific values, such as service keys or configuration descriptions, * be evaluated by Effect and yielded inside `Effect.gen`. * @@ -8,7 +8,7 @@ */ import type * as Effect from "./Effect.ts" import type * as Fiber from "./Fiber.ts" -import { evaluate, makePrimitiveProto } from "./internal/core.ts" +import { type EffectTypeId, evaluate, makePrimitiveProto } from "./internal/core.ts" /** * Create a low-level `Effect` prototype. @@ -24,6 +24,7 @@ import { evaluate, makePrimitiveProto } from "./internal/core.ts" * When the effect is evaluated, it calls `evaluate` with the current fiber. * * @see {@link Class} for a class-based approach to defining custom Effect values + * @see {@link Mixin} for wrapping an existing class constructor * * @category prototypes * @since 4.0.0 @@ -40,14 +41,16 @@ export const Prototype = >(options: { [evaluate]: options.evaluate }) as any +const proto = Prototype>({ + label: "Effectable", + evaluate(_) { + return this.asEffect() + } +}) + const Base: new() => Effect.Effect = (() => { const Base = function() {} - Base.prototype = Prototype({ - label: "Effectable", - evaluate(_) { - return this - } - }) + Base.prototype = proto return Base as any })() @@ -60,9 +63,75 @@ const Base: new() => Effect.Effect = (() => { * as `Effect` values. * * @see {@link Prototype} for a lower-level primitive approach to creating custom Effect-like values without a class + * @see {@link Mixin} for wrapping an existing class constructor * @category constructors * @since 2.0.0 */ export abstract class Class extends Base { - abstract override: Effect.Effect + abstract asEffect(): Effect.Effect +} + +type AsEffectReturn = Self extends { + asEffect(): infer A extends Effect.Effect +} ? A + : never + +declare abstract class MixinBase extends Class { + constructor(...args: ReadonlyArray) + override readonly [EffectTypeId]: AsEffectReturn[typeof EffectTypeId] + override [Symbol.iterator](): Effect.EffectIterator> +} + +/** + * Returns a subclass of the provided class that inserts the Effect prototype + * into the inheritance chain. + * + * **When to use** + * + * Use to make instances of an existing class behave as `Effect` values without + * extending {@link Class} or modifying the original prototype. + * + * **Details** + * + * Pass the class to wrap, then implement `asEffect` on the final class. The + * returned class is abstract, and the success, error, and service types are + * inferred from the concrete `asEffect` return type. Concrete and abstract base + * classes are supported. Constructor parameters and instance members are + * preserved, except that Effect's prototype members shadow base prototype + * members with the same name: `pipe`, `toString`, `toJSON`, `[Symbol.iterator]`, + * and `[Symbol.for("nodejs.util.inspect.custom")]`. + * + * **Example** (Evaluating a mixed-in class) + * + * ```ts import.meta.vitest + * import { Effect, Effectable } from "effect" + * + * class Box { + * constructor(readonly value: number) {} + * } + * + * class EffectBox extends Effectable.Mixin(Box) { + * asEffect() { + * return Effect.succeed(this.value) + * } + * } + * + * const box = new EffectBox(2) + * Effect.isEffect(box) // => true + * await Effect.runPromise(box) // => 2 + * ``` + * + * @see {@link Prototype} for a lower-level primitive approach to creating custom Effect-like values without a class + * @see {@link Class} for a base constructor to extend + * @category constructors + * @since 4.0.0 + */ +export const Mixin = ) => object>( + klass: TBase +): TBase & typeof MixinBase => { + abstract class Mixed extends klass { + abstract asEffect(): Effect.Effect + } + Object.defineProperties(Mixed.prototype, Object.getOwnPropertyDescriptors(proto)) + return Mixed as TBase & typeof MixinBase } diff --git a/repos/effect/packages/effect/src/Encoding.ts b/repos/effect/packages/effect/src/Encoding.ts index 5e6a77844d..25dcc13127 100644 --- a/repos/effect/packages/effect/src/Encoding.ts +++ b/repos/effect/packages/effect/src/Encoding.ts @@ -34,7 +34,7 @@ import * as Result from "./Result.ts" * @category type IDs * @since 4.0.0 */ -export const EncodingErrorTypeId = "~effect/encoding/EncodingError" as const +export const EncodingErrorTypeId = "~effect/Encoding/EncodingError" as const /** * Literal type of the `EncodingErrorTypeId` marker. @@ -409,6 +409,8 @@ export const encodeHex: (input: Uint8Array | string) => string = (input) => * Generates a random lowercase hexadecimal string, optimized for lengths that * are multiples of 8. * + * **Details** + * * `length` is not validated. The function generates `length >>> 3` random * 8-character words, so non-negative lengths below `2 ** 32` are rounded down * to a multiple of 8 and other values follow JavaScript's unsigned 32-bit @@ -422,13 +424,104 @@ export const encodeHex: (input: Uint8Array | string) => string = (input) => * @since 4.0.0 */ export const randomHex = (length: number): string => { - let result = "" - for (let i = length >>> 3; i > 0; i--) { - const word = (Math.random() * 0x100000000) >>> 0 - result += byteToHex[word >>> 24] + byteToHex[(word >>> 16) & 0xff] + byteToHex[(word >>> 8) & 0xff] + - byteToHex[word & 0xff] + switch (length) { + case 16: + return randomHex16() + case 32: + return randomHex32() + default: { + let result = "" + for (let i = length >>> 3; i > 0; i--) { + result += randomHex8() + } + return result + } } - return result +} + +const hexCharCodes = Uint8Array.from("0123456789abcdef", (c) => c.charCodeAt(0)) + +const randomWord = (): number => (Math.random() * 0x100000000) >>> 0 + +// Trace and span identifiers are the common lengths. A single +// String.fromCharCode call produces a flat string, which avoids rope +// flattening when the identifier is later serialized. +const randomHex8 = (): string => { + const a = randomWord() + return String.fromCharCode( + hexCharCodes[a >>> 28], + hexCharCodes[(a >>> 24) & 15], + hexCharCodes[(a >>> 20) & 15], + hexCharCodes[(a >>> 16) & 15], + hexCharCodes[(a >>> 12) & 15], + hexCharCodes[(a >>> 8) & 15], + hexCharCodes[(a >>> 4) & 15], + hexCharCodes[a & 15] + ) +} + +const randomHex16 = (): string => { + const a = randomWord() + const b = randomWord() + return String.fromCharCode( + hexCharCodes[a >>> 28], + hexCharCodes[(a >>> 24) & 15], + hexCharCodes[(a >>> 20) & 15], + hexCharCodes[(a >>> 16) & 15], + hexCharCodes[(a >>> 12) & 15], + hexCharCodes[(a >>> 8) & 15], + hexCharCodes[(a >>> 4) & 15], + hexCharCodes[a & 15], + hexCharCodes[b >>> 28], + hexCharCodes[(b >>> 24) & 15], + hexCharCodes[(b >>> 20) & 15], + hexCharCodes[(b >>> 16) & 15], + hexCharCodes[(b >>> 12) & 15], + hexCharCodes[(b >>> 8) & 15], + hexCharCodes[(b >>> 4) & 15], + hexCharCodes[b & 15] + ) +} + +const randomHex32 = (): string => { + const a = randomWord() + const b = randomWord() + const c = randomWord() + const d = randomWord() + return String.fromCharCode( + hexCharCodes[a >>> 28], + hexCharCodes[(a >>> 24) & 15], + hexCharCodes[(a >>> 20) & 15], + hexCharCodes[(a >>> 16) & 15], + hexCharCodes[(a >>> 12) & 15], + hexCharCodes[(a >>> 8) & 15], + hexCharCodes[(a >>> 4) & 15], + hexCharCodes[a & 15], + hexCharCodes[b >>> 28], + hexCharCodes[(b >>> 24) & 15], + hexCharCodes[(b >>> 20) & 15], + hexCharCodes[(b >>> 16) & 15], + hexCharCodes[(b >>> 12) & 15], + hexCharCodes[(b >>> 8) & 15], + hexCharCodes[(b >>> 4) & 15], + hexCharCodes[b & 15], + hexCharCodes[c >>> 28], + hexCharCodes[(c >>> 24) & 15], + hexCharCodes[(c >>> 20) & 15], + hexCharCodes[(c >>> 16) & 15], + hexCharCodes[(c >>> 12) & 15], + hexCharCodes[(c >>> 8) & 15], + hexCharCodes[(c >>> 4) & 15], + hexCharCodes[c & 15], + hexCharCodes[d >>> 28], + hexCharCodes[(d >>> 24) & 15], + hexCharCodes[(d >>> 20) & 15], + hexCharCodes[(d >>> 16) & 15], + hexCharCodes[(d >>> 12) & 15], + hexCharCodes[(d >>> 8) & 15], + hexCharCodes[(d >>> 4) & 15], + hexCharCodes[d & 15] + ) } /** @@ -769,10 +862,7 @@ const base64UrlEncodeUint8Array = (data: Uint8Array) => // Hex internals -const byteToHex: Array = [] -for (let i = 0; i < 256; i++) { - byteToHex.push(i.toString(16).padStart(2, "0")) -} +const byteToHex: Array = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0")) const hexEncodeUint8Array = (bytes: Uint8Array): string => { let result = "" diff --git a/repos/effect/packages/effect/src/Equal.ts b/repos/effect/packages/effect/src/Equal.ts index 0b45a6ad3c..f696fe444f 100644 --- a/repos/effect/packages/effect/src/Equal.ts +++ b/repos/effect/packages/effect/src/Equal.ts @@ -52,7 +52,7 @@ import { hasProperty } from "./Predicate.ts" * @category symbols * @since 2.0.0 */ -export const symbol = "~effect/interfaces/Equal" +export const symbol = "~effect/Equal" /** * The interface for types that define their own equality logic. diff --git a/repos/effect/packages/effect/src/ExecutionPlan.ts b/repos/effect/packages/effect/src/ExecutionPlan.ts index 2360043593..135385f765 100644 --- a/repos/effect/packages/effect/src/ExecutionPlan.ts +++ b/repos/effect/packages/effect/src/ExecutionPlan.ts @@ -289,6 +289,9 @@ const Proto: Omit, "steps"> = { return effect.contextWith((context: Context.Context) => effect.succeed(makeProto(self.steps.map((step) => ({ ...step, + while: step.while + ? (input: any) => effect.provideContext(step.while!(input), context) + : undefined, provide: Layer.isLayer(step.provide) ? Layer.provide(step.provide, Layer.succeedContext(context)) : step.provide diff --git a/repos/effect/packages/effect/src/Fiber.ts b/repos/effect/packages/effect/src/Fiber.ts index 4e511ee277..4eae1625ca 100644 --- a/repos/effect/packages/effect/src/Fiber.ts +++ b/repos/effect/packages/effect/src/Fiber.ts @@ -13,12 +13,13 @@ import type { Effect } from "./Effect.ts" import type { Exit } from "./Exit.ts" import * as effect from "./internal/effect.ts" import type { LogLevel } from "./LogLevel.ts" +import type { FiberRuntimeMetricsService } from "./Metric.ts" import type { Pipeable } from "./Pipeable.ts" import { hasProperty } from "./Predicate.ts" import type { StackFrame } from "./References.ts" import type { Scheduler, SchedulerDispatcher } from "./Scheduler.ts" import type { Scope } from "./Scope.ts" -import type { AnySpan } from "./Tracer.ts" +import type { AnySpan, Tracer } from "./Tracer.ts" import type { Covariant } from "./Types.ts" const TypeId = "~effect/Fiber" @@ -75,14 +76,8 @@ export interface Fiber extends Pipeable { readonly getRef: (ref: Context.Reference) => A readonly context: Context.Context setContext(context: Context.Context): void - readonly currentScheduler: Scheduler + readonly cache: Fiber.Cache readonly currentDispatcher: SchedulerDispatcher - readonly currentSpan?: AnySpan | undefined - readonly currentLogLevel: LogLevel - readonly minimumLogLevel: LogLevel - readonly currentStackFrame?: StackFrame | undefined - readonly maxOpsBeforeYield: number - readonly currentPreventYield: boolean readonly addObserver: (cb: (exit: Exit) => void) => () => void readonly interruptUnsafe: ( fiberId?: number | undefined, @@ -155,6 +150,36 @@ export declare namespace Fiber { readonly _A: Covariant readonly _E: Covariant } + + /** + * Context-derived values cached for the fiber's current `Context`. + * + * **When to use** + * + * Use to read runtime services resolved from the fiber's context, such as + * the scheduler, current span, or log levels. + * + * **Details** + * + * The cache object is computed once per context cache root and shared by + * every fiber running with that root, so it must be treated as immutable. + * + * @category models + * @since 4.0.0 + */ + export interface Cache { + readonly scheduler: Scheduler + readonly tracer: Tracer | undefined + readonly tracerContext: Tracer["context"] | undefined + readonly tracerEnabled: boolean + readonly span: AnySpan | undefined + readonly logLevel: LogLevel + readonly minimumLogLevel: LogLevel + readonly stackFrame: StackFrame | undefined + readonly runtimeMetrics: FiberRuntimeMetricsService | undefined + readonly maxOpsBeforeYield: number + readonly preventYield: boolean + } } const await_: (self: Fiber) => Effect> = effect.fiberAwait diff --git a/repos/effect/packages/effect/src/FiberHandle.ts b/repos/effect/packages/effect/src/FiberHandle.ts index 7a004bfa15..2317b7a424 100644 --- a/repos/effect/packages/effect/src/FiberHandle.ts +++ b/repos/effect/packages/effect/src/FiberHandle.ts @@ -332,10 +332,10 @@ export const setUnsafe: { fiber.interruptUnsafe(internalFiberId) return } else if (self.state.fiber !== undefined) { - if (options?.onlyIfMissing === true) { - fiber.interruptUnsafe(internalFiberId) + if (self.state.fiber === fiber) { return - } else if (self.state.fiber === fiber) { + } else if (options?.onlyIfMissing === true) { + fiber.interruptUnsafe(internalFiberId) return } self.state.fiber.interruptUnsafe(internalFiberId) diff --git a/repos/effect/packages/effect/src/FiberMap.ts b/repos/effect/packages/effect/src/FiberMap.ts index 828632ac55..a8b7f1fbe1 100644 --- a/repos/effect/packages/effect/src/FiberMap.ts +++ b/repos/effect/packages/effect/src/FiberMap.ts @@ -305,7 +305,8 @@ const isInternalInterruption = Filter.toPredicate(Filter.compose( * * When the fiber completes, it is removed from the map. If the key already has * a fiber, that previous fiber is interrupted unless `onlyIfMissing` is set; - * in that case the new fiber is interrupted and the existing entry is kept. + * in that case a different new fiber is interrupted and the existing entry is + * kept, while re-registering the existing fiber is a no-op. * * **Example** (Adding a fiber unsafely) * @@ -367,16 +368,19 @@ export const setUnsafe: { const previous = MutableHashMap.get(self.state.backing, key) if (previous._tag === "Some") { - if (options?.onlyIfMissing === true) { - fiber.interruptUnsafe(internalFiberId) + if (previous.value === fiber) { return - } else if (previous.value === fiber) { + } else if (options?.onlyIfMissing === true) { + fiber.interruptUnsafe(internalFiberId) return } - previous.value.interruptUnsafe(internalFiberId) } + // Install the replacement before interruption can re-enter the map through a finalizer. MutableHashMap.set(self.state.backing, key, fiber) + if (previous._tag === "Some") { + previous.value.interruptUnsafe(internalFiberId) + } fiber.addObserver((exit) => { if (self.state._tag === "Closed") { return @@ -405,7 +409,8 @@ export const setUnsafe: { * * When the fiber completes, it is removed from the map. If the key already has * a fiber, that previous fiber is interrupted unless `onlyIfMissing` is set; - * in that case the new fiber is interrupted and the existing entry is kept. + * in that case a different new fiber is interrupted and the existing entry is + * kept, while re-registering the existing fiber is a no-op. * * This is the Effect-wrapped version of `setUnsafe`. * diff --git a/repos/effect/packages/effect/src/FileSystem.ts b/repos/effect/packages/effect/src/FileSystem.ts index 5cca2154df..adb2a03a96 100644 --- a/repos/effect/packages/effect/src/FileSystem.ts +++ b/repos/effect/packages/effect/src/FileSystem.ts @@ -5,13 +5,13 @@ * Platform packages provide concrete layers, while this module defines the * operations for reading, writing, inspecting, streaming, and watching files. * Operations return `Effect`, `Stream`, or `Sink` values and fail with - * `PlatformError`. The module also includes file handles, size helpers, open - * flags, watch events, and the watch backend service. + * `PlatformError`. The module also includes file handles, open flags, watch + * events, and the watch backend service. * * @since 4.0.0 */ import * as Arr from "./Array.ts" -import type * as Brand from "./Brand.ts" +import * as ByteSize from "./ByteSize.ts" import * as Cause from "./Cause.ts" import * as Context from "./Context.ts" import * as Effect from "./Effect.ts" @@ -25,7 +25,7 @@ import type { Scope } from "./Scope.ts" import * as Sink from "./Sink.ts" import * as Stream from "./Stream.ts" -const TypeId = "~effect/platform/FileSystem" +const TypeId = "~effect/FileSystem" /** * Core interface for file system operations in Effect. @@ -39,12 +39,12 @@ const TypeId = "~effect/platform/FileSystem" * **Example** (Accessing file system operations) * * ```ts import.meta.vitest - * import { Effect, FileSystem } from "effect" + * import { ByteSize, Effect, FileSystem } from "effect" * * const fileSystem = FileSystem.makeNoop({ * exists: () => Effect.succeed(true), * makeDirectory: () => Effect.void, - * stat: () => Effect.succeed({ size: FileSystem.Size(22) } as FileSystem.File.Info), + * stat: () => Effect.succeed({ size: ByteSize.bytes(22) } as FileSystem.File.Info), * readFileString: () => Effect.succeed("{\"env\": \"development\"}") * }) * @@ -68,7 +68,7 @@ const TypeId = "~effect/platform/FileSystem" * }) * * const result = Effect.runSync(Effect.provideService(program, FileSystem.FileSystem, fileSystem)) - * result.size // => 22n + * ByteSize.toBigInt(result.size) // => 22n * result.content // => "{\"env\": \"development\"}" * ``` * @@ -299,7 +299,7 @@ export interface FileSystem { } ) => Sink.Sink /** - * Get information about a file at `path`. + * Get information about a file at `path`. See `File.Info` for metadata limits. */ readonly stat: ( path: string @@ -321,9 +321,9 @@ export interface FileSystem { readonly stream: ( path: string, options?: { - readonly bytesToRead?: SizeInput | undefined - readonly chunkSize?: SizeInput | undefined - readonly offset?: SizeInput | undefined + readonly bytesToRead?: ByteSize.Input | undefined + readonly chunkSize?: number | undefined + readonly offset?: ByteSize.Input | undefined } ) => Stream.Stream /** @@ -339,7 +339,7 @@ export interface FileSystem { */ readonly truncate: ( path: string, - length?: SizeInput + length?: number ) => Effect.Effect /** * Change the file system timestamps of the file at `path`. @@ -383,199 +383,6 @@ export interface FileSystem { ) => Effect.Effect } -/** - * Represents a file size in bytes using a branded bigint. - * - * **Details** - * - * This type ensures type safety when working with file sizes, preventing - * accidental mixing of regular numbers with size values. The underlying - * bigint allows for handling very large file sizes beyond JavaScript's - * number precision limits. - * - * **Example** (Creating branded file sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.Size(1024) // => 1024n - * FileSystem.Size(BigInt("9007199254740992")) // => 9007199254740992n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export type Size = Brand.Branded - -/** - * Input type for size parameters that accepts multiple numeric types. - * - * **Details** - * - * This union type allows file system operations to accept size values in - * different formats for convenience, which are then normalized to the - * branded `Size` type internally. - * - * **Example** (Using size inputs) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * const inputs: ReadonlyArray = [ - * 1024, - * 2048n, - * FileSystem.Size(4096) - * ] - * inputs.map(FileSystem.Size) // => [1024n, 2048n, 4096n] - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export type SizeInput = bigint | number | Size - -/** - * Creates a `Size` from various numeric input types. - * - * **Details** - * - * Converts numbers, bigints, or existing Size values into a properly - * branded Size type. This function handles the conversion and ensures - * type safety for file size operations. - * - * **Example** (Converting size inputs) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * // From number - * const size1 = FileSystem.Size(1024) - * typeof size1 // => "bigint" - * - * // From bigint - * const size2 = FileSystem.Size(BigInt(2048)) - * - * // From existing Size (identity) - * const size3 = FileSystem.Size(size1) - * const sizes = [size2, size3] // => [2048n, 1024n] - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const Size = (bytes: SizeInput): Size => typeof bytes === "bigint" ? bytes as Size : BigInt(bytes) as Size - -/** - * Creates a `Size` representing kilobytes (1024 bytes). - * - * **Details** - * - * Converts a number of kilobytes to the equivalent size in bytes. - * Uses binary kilobytes (1024 bytes) rather than decimal (1000 bytes). - * - * **Example** (Creating kibibyte sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.KiB(64) // => 65536n - * FileSystem.KiB(100) // => 102400n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const KiB = (n: number): Size => Size(n * 1024) - -/** - * Creates a `Size` representing mebibytes (1024² bytes). - * - * **Details** - * - * Converts a number of mebibytes to the equivalent size in bytes. - * Uses binary mebibytes (1,048,576 bytes) rather than decimal megabytes. - * - * **Example** (Creating mebibyte sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.MiB(10) // => 10485760n - * FileSystem.MiB(100) // => 104857600n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const MiB = (n: number): Size => Size(n * 1024 * 1024) - -/** - * Creates a `Size` representing gibibytes (1024³ bytes). - * - * **Details** - * - * Converts a number of gibibytes to the equivalent size in bytes. - * Uses binary gibibytes (1,073,741,824 bytes) rather than decimal gigabytes. - * - * **Example** (Creating gibibyte sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.GiB(1) // => 1073741824n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const GiB = (n: number): Size => Size(n * 1024 * 1024 * 1024) - -/** - * Creates a `Size` representing tebibytes (1024⁴ bytes). - * - * **Details** - * - * Converts a number of tebibytes to the equivalent size in bytes. - * Uses binary tebibytes (1,099,511,627,776 bytes) rather than decimal terabytes. - * - * **Example** (Creating tebibyte sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.TiB(1) // => 1099511627776n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const TiB = (n: number): Size => Size(n * 1024 * 1024 * 1024 * 1024) - -const bigint1024 = BigInt(1024) -const bigintPiB = bigint1024 * bigint1024 * bigint1024 * bigint1024 * bigint1024 - -/** - * Creates a `Size` representing pebibytes (1024⁵ bytes). - * - * **Details** - * - * Converts a number of pebibytes to the equivalent size in bytes. - * Uses binary pebibytes (1,125,899,906,842,624 bytes) rather than decimal petabytes. - * This function uses BigInt arithmetic to handle the very large numbers involved. - * - * **Example** (Creating pebibyte sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.PiB(2) // => 2251799813685248n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const PiB = (n: number): Size => Size(BigInt(n) * bigintPiB) - /** * File open flags that determine how a file is opened and what operations are allowed. * @@ -660,7 +467,7 @@ export type OpenFlag = * @category services * @since 4.0.0 */ -export const FileSystem: Context.Service = Context.Service("effect/platform/FileSystem") +export const FileSystem: Context.Service = Context.Service("effect/FileSystem") /** * Creates a FileSystem implementation from a partial implementation. @@ -712,12 +519,16 @@ export const make = ( })), stream: Effect.fnUntraced(function*(path, options) { const file = yield* impl.open(path, { flag: "r" }) - if (options?.offset) { - yield* file.seek(options.offset, "start") + const offset = options?.offset === undefined ? undefined : ByteSize.fromInputUnsafe(options.offset) + if (offset) { + yield* file.seek(offset, "start") } - const bytesToRead = options?.bytesToRead !== undefined ? Size(options.bytesToRead) : undefined + const bytesToRead = options?.bytesToRead === undefined + ? undefined + : ByteSize.fromInputUnsafe(options.bytesToRead) let totalBytesRead = BigInt(0) - const chunkSize = Size(options?.chunkSize ?? 64 * 1024) + // Validate chunk sizes even for zero-byte reads. + const chunkSize = Number(BigInt(options?.chunkSize ?? 64 * 1024)) const readChunk = file.readAlloc(chunkSize) return Stream.fromPull(Effect.succeed( Effect.flatMap( @@ -726,7 +537,7 @@ export const make = ( return Cause.done() } return bytesToRead !== undefined && (bytesToRead - totalBytesRead) < chunkSize - ? file.readAlloc(bytesToRead - totalBytesRead) + ? file.readAlloc(Number(bytesToRead - totalBytesRead)) : readChunk }), Option.match({ @@ -741,7 +552,7 @@ export const make = ( }, Stream.unwrap), sink: (path, options) => pipe( - impl.open(path, { flag: "w", ...options }), + impl.open(path, { ...options, flag: options?.flag ?? "w" }), Effect.map((file) => Sink.forEach((_: Uint8Array) => file.writeAll(_))), Sink.unwrap ), @@ -969,7 +780,7 @@ export const layerNoop = (fileSystem: Partial): Layer.Layer hasProperty(u, FileTypeId) * **Example** (Working with file handles) * * ```ts import.meta.vitest - * import { Effect, FileSystem, Option } from "effect" + * import { ByteSize, Effect, FileSystem, Option } from "effect" * * const file: FileSystem.File = { * [FileSystem.FileTypeId]: FileSystem.FileTypeId, - * stat: Effect.succeed({ size: FileSystem.Size(5) } as FileSystem.File.Info), - * seek: () => Effect.succeed(FileSystem.Size(0)), + * stat: Effect.succeed({ size: ByteSize.bytes(5) } as FileSystem.File.Info), + * seek: () => Effect.succeed(BigInt(0)), * sync: Effect.void, * read: (buffer) => Effect.sync(() => { * buffer.set([1, 2, 3, 4, 5]) - * return FileSystem.Size(5) + * return 5 * }), * readAlloc: () => Effect.succeed(Option.none()), * truncate: () => Effect.void, - * write: (buffer) => Effect.succeed(FileSystem.Size(buffer.length)), + * write: (buffer) => Effect.succeed(buffer.length), * writeAll: () => Effect.void * } * @@ -1031,7 +842,10 @@ export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId) * return { size: stats.size, bytesRead, buffer: Array.from(buffer) } * }) * - * Effect.runSync(program) // => { size: 5n, bytesRead: 5n, buffer: [1, 2, 3, 4, 5] } + * const result = Effect.runSync(program) + * ByteSize.toBigInt(result.size) // => 5n + * result.bytesRead // => 5 + * result.buffer // => [1, 2, 3, 4, 5] * ``` * * @category models @@ -1039,13 +853,19 @@ export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId) */ export interface File { readonly [FileTypeId]: typeof FileTypeId + /** + * Get information about the open file. See `File.Info` for metadata limits. + */ readonly stat: Effect.Effect - readonly seek: (offset: SizeInput, from: SeekMode) => Effect.Effect + /** + * Seeks before the start fail with `BadArgument` and leave the cursor unchanged. + */ + readonly seek: (offset: bigint, from: SeekMode) => Effect.Effect readonly sync: Effect.Effect - readonly read: (buffer: Uint8Array) => Effect.Effect - readonly readAlloc: (size: SizeInput) => Effect.Effect, PlatformError> - readonly truncate: (length?: SizeInput) => Effect.Effect - readonly write: (buffer: Uint8Array) => Effect.Effect + readonly read: (buffer: Uint8Array) => Effect.Effect + readonly readAlloc: (size: number) => Effect.Effect, PlatformError> + readonly truncate: (length?: number) => Effect.Effect + readonly write: (buffer: Uint8Array) => Effect.Effect readonly writeAll: (buffer: Uint8Array) => Effect.Effect } @@ -1086,10 +906,15 @@ export declare namespace File { * permissions, and size information. This structure is returned by file * stat operations. * + * Node and Bun preserve `size` and `blksize` exactly. Unsafe numeric metadata + * (such as `ino` or `dev`) fails the entire stat operation with `BadArgument`, + * including optional fields. Inode values above `Number.MAX_SAFE_INTEGER` + * can therefore prevent stat and HTTP file serving even for small files. + * * **Example** (Inspecting file information) * * ```ts import.meta.vitest - * import { FileSystem, Option } from "effect" + * import { ByteSize, FileSystem, Option } from "effect" * * const info: FileSystem.File.Info = { * type: "File", @@ -1103,13 +928,13 @@ export declare namespace File { * uid: Option.none(), * gid: Option.none(), * rdev: Option.none(), - * size: FileSystem.Size(5), + * size: ByteSize.bytes(5), * blksize: Option.none(), * blocks: Option.none() * } * * info.type // => "File" - * info.size // => 5n + * ByteSize.toBigInt(info.size) // => 5n * info.mode.toString(8) // => "644" * * const modified = Option.match(info.mtime, { @@ -1135,8 +960,8 @@ export declare namespace File { readonly uid: Option.Option readonly gid: Option.Option readonly rdev: Option.Option - readonly size: Size - readonly blksize: Option.Option + readonly size: ByteSize.ByteSize + readonly blksize: Option.Option readonly blocks: Option.Option } } @@ -1296,4 +1121,4 @@ export class WatchBackend extends Context.Service Option.Option> -}>()("effect/platform/FileSystem/WatchBackend") {} +}>()("effect/FileSystem/WatchBackend") {} diff --git a/repos/effect/packages/effect/src/Formatter.ts b/repos/effect/packages/effect/src/Formatter.ts index db202ef930..9121c252de 100644 --- a/repos/effect/packages/effect/src/Formatter.ts +++ b/repos/effect/packages/effect/src/Formatter.ts @@ -57,6 +57,7 @@ export interface Formatter { * - Handles `BigInt`, `Symbol`, `Set`, `Map`, `Date`, `RegExp`, and class * instances that `JSON.stringify` cannot represent. * - Circular references are shown as `"[Circular]"` instead of throwing. + * - Failures while inspecting a value are rendered as diagnostic placeholders instead of throwing. * - Primitives: stringified naturally (`null`, `undefined`, `123`, `true`). * Strings are JSON-quoted. * - Objects with a custom `toString` (not `Object.prototype.toString`): @@ -127,6 +128,15 @@ export function format(input: unknown, options?: { } function recur(v: unknown, d = 0): string { + try { + return recurUnsafe(v, d) + } catch { + if ((typeof v === "object" && v !== null) || typeof v === "function") ancestors.delete(v) + return "[inspection threw]" + } + } + + function recurUnsafe(v: unknown, d = 0): string { if (typeof v === "string") return JSON.stringify(v) if ( @@ -159,17 +169,17 @@ export function format(input: unknown, options?: { v["toString"] !== Array.prototype.toString ) { const s = safeToString(v) - output = v instanceof Error && v.cause ? `${s} (cause: ${recur(v.cause, d)})` : s + output = v instanceof Error && v.cause !== undefined ? `${s} (cause: ${recur(v.cause, d)})` : s } else if (Symbol.iterator in v) { output = `${v.constructor.name}(${recur(Array.from(v as any), d)})` } else { const keys = ownKeys(v) if (!gap || keys.length <= 1) { - const body = `{${keys.map((k) => `${formatPropertyKey(k)}:${recur((v as any)[k], d)}`).join(",")}}` + const body = `{${keys.map((k) => `${formatPropertyKey(k)}:${recur(safeGet(v, k), d)}`).join(",")}}` output = wrap(v, body) } else { const body = `{\n${ - keys.map((k) => `${ind(d + 1)}${formatPropertyKey(k)}: ${recur((v as any)[k], d + 1)}`).join(",\n") + keys.map((k) => `${ind(d + 1)}${formatPropertyKey(k)}: ${recur(safeGet(v, k), d + 1)}`).join(",\n") }\n${ind(d)}}` output = wrap(v, body) } @@ -225,6 +235,14 @@ function safeToString(input: any): string { } } +function safeGet(input: object, key: PropertyKey): unknown { + try { + return (input as any)[key] + } catch { + return "[property access threw]" + } +} + /** * Stringifies a value to JSON safely, silently dropping circular references. * diff --git a/repos/effect/packages/effect/src/Graph.ts b/repos/effect/packages/effect/src/Graph.ts index 4a099e3056..7f9ae698be 100644 --- a/repos/effect/packages/effect/src/Graph.ts +++ b/repos/effect/packages/effect/src/Graph.ts @@ -19,7 +19,6 @@ import * as csr from "./internal/graphCsr.ts" import * as MutableHashMap from "./MutableHashMap.ts" import * as Option from "./Option.ts" import type { Pipeable } from "./Pipeable.ts" -import { hasProperty } from "./Predicate.ts" import type { Covariant, Invariant } from "./Types.ts" const TypeId = internal.TypeId @@ -147,22 +146,6 @@ export interface Snapshot { readonly edges: ReadonlyArray> } -/** - * Common public protocol for graph values. - * - * **Details** - * - * Contains only the runtime marker and shared protocols. Graph storage is kept - * internal; use module functions such as `nodes`, `edges`, `getNode`, and - * `getEdge` to inspect graph contents. - * - * @category protocols - * @since 3.18.0 - */ -export interface Proto extends Iterable, Equal.Equal, Pipeable, Inspectable { - readonly [TypeId]: Graph.Variance -} - /** * Immutable graph interface. * @@ -181,7 +164,10 @@ export interface Proto extends Iterable, * @category models * @since 3.18.0 */ -export interface Graph extends Proto { +export interface Graph + extends Iterable, Equal.Equal, Pipeable, Inspectable +{ + readonly [TypeId]: Graph.Variance readonly type: T readonly mutable: false } @@ -415,9 +401,9 @@ const withMutationGuard = ( * @category guards * @since 4.0.0 */ -export const isGraph = ( +export const isGraph: ( u: U | Graph | MutableGraph -): u is Graph | MutableGraph => hasProperty(u, TypeId) +) => u is Graph | MutableGraph = internal.isGraph /** * Reconstructs an immutable graph from its indexed active structure. @@ -777,7 +763,7 @@ const mutateScoped = ( * * **When to use** * - * Use for the usual immutable update workflow when several node or edge + * Use when several node or edge * mutations should be applied together. * * **Details** @@ -3031,6 +3017,8 @@ export const edgeCount = ( /** * Returns the indices of all edges incident to a node. * + * **Details** + * * Each edge is returned once in graph edge order, including self-loops. * Throws a `GraphError` when the node does not exist. * @@ -3093,6 +3081,8 @@ export const incidentEdges: { /** * Returns the indices of outgoing edges for a node in a directed graph. * + * **Details** + * * Parallel edges and self-loops are returned separately in adjacency order. * Throws a `GraphError` for an undirected graph or missing node. * @@ -3124,6 +3114,8 @@ export const outgoingEdges: { /** * Returns the indices of incoming edges for a node in a directed graph. * + * **Details** + * * Parallel edges and self-loops are returned separately in reverse-adjacency * order. Throws a `GraphError` for an undirected graph or missing node. * @@ -3155,6 +3147,8 @@ export const incomingEdges: { /** * Returns all edge indices connecting the supplied nodes. * + * **Details** + * * Directed graphs only include edges from `source` to `target`; undirected * graphs include either stored orientation. Parallel edges are retained. * Throws a `GraphError` when either node does not exist. @@ -3202,6 +3196,8 @@ export const edgesBetween: { /** * Returns the degree of a node in an undirected graph. * + * **Details** + * * Parallel edges count separately and a self-loop contributes two. Throws a * `GraphError` for a directed graph or missing node. * @@ -3230,6 +3226,8 @@ export const degree: { /** * Returns the out-degree of a node in a directed graph. * + * **Details** + * * Parallel edges count separately and a self-loop contributes one. Throws a * `GraphError` for an undirected graph or missing node. * @@ -3258,6 +3256,8 @@ export const outDegree: { /** * Returns the in-degree of a node in a directed graph. * + * **Details** + * * Parallel edges count separately and a self-loop contributes one. Throws a * `GraphError` for an undirected graph or missing node. * @@ -6927,7 +6927,7 @@ export const bellmanFord: { const edges = csr.getEdges(cache) const edgeIds = csr.getEdgeIds(cache) const edgeCache = csr.getEdgeEndpoints(cache) - const outgoing = csr.getOutgoing(cache) + const outgoing = csr.getOutgoingWithEdges(cache) const source = csr.getNodeIndex(cache, config.source)! const target = csr.getNodeIndex(cache, config.target)! const weights = new Float64Array(edges.length) @@ -7020,7 +7020,9 @@ export const bellmanFord: { while (head < tail) { const node = queue[head++] for (let i = outgoing.rowOffsets[node]; i < outgoing.rowOffsets[node + 1]; i++) { - markAffected(outgoing.columnIndices[i]) + if (weights[outgoing.edgeIndices[i]] !== Infinity) { + markAffected(outgoing.columnIndices[i]) + } } } } diff --git a/repos/effect/packages/effect/src/Hash.ts b/repos/effect/packages/effect/src/Hash.ts index 0a12cca93d..99f226526e 100644 --- a/repos/effect/packages/effect/src/Hash.ts +++ b/repos/effect/packages/effect/src/Hash.ts @@ -28,7 +28,7 @@ import { hasProperty } from "./Predicate.ts" * @category symbols * @since 2.0.0 */ -export const symbol = "~effect/interfaces/Hash" +export const symbol = "~effect/Hash" /** * A type that represents an object that can be hashed. @@ -106,10 +106,6 @@ export const hash: (self: A) => number = (self: A) => { return number(self) case "bigint": return string(self.toString(10)) - case "boolean": - return string(String(self)) - case "symbol": - return string(String(self)) case "string": return string(self) case "undefined": @@ -153,9 +149,8 @@ export const hash: (self: A) => number = (self: A) => { } } default: - throw new Error( - `BUG: unhandled typeof ${typeof self} - please report an issue at https://github.com/Effect-TS/effect/issues` - ) + // The remaining primitive types are boolean and symbol. + return string(String(self)) } } @@ -318,14 +313,8 @@ export const isHash = (u: unknown): u is Hash => hasProperty(u, symbol) * @since 2.0.0 */ export const number = (n: number) => { - if (n !== n) { - return string("NaN") - } - if (n === Infinity) { - return string("Infinity") - } - if (n === -Infinity) { - return string("-Infinity") + if (n !== n || n === Infinity || n === -Infinity) { + return string(String(n)) } let h = n | 0 if (h !== n) { diff --git a/repos/effect/packages/effect/src/HashRing.ts b/repos/effect/packages/effect/src/HashRing.ts index f85ac52fa4..789bf05000 100644 --- a/repos/effect/packages/effect/src/HashRing.ts +++ b/repos/effect/packages/effect/src/HashRing.ts @@ -12,12 +12,13 @@ import { dual } from "./Function.ts" import * as Hash from "./Hash.ts" import { PipeInspectableProto } from "./internal/core.ts" +import * as Count from "./internal/count.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 +const TypeId = "~effect/HashRing" as const /** * A weighted consistent-hashing ring for assigning inputs to nodes with stable @@ -312,6 +313,11 @@ export const get = (self: HashRing, input: s * Use to precompute ownership for a fixed number of shard indexes across the * current ring members. * + * **Details** + * + * Finite fractional values of `count` are rounded down. `NaN` and non-positive + * values produce an empty shard distribution. + * * @category combinators * @since 3.19.0 */ @@ -319,6 +325,7 @@ export const getShards = (self: HashRing, co if (self.ring.length === 0) { return undefined } + count = Count.normalize(count) const shards = new Array(count) @@ -413,7 +420,7 @@ function getIndexForInput( return [a, distA] } const range = Math.max(lo, len - lo) - for (let i = 1; i < range; i++) { + for (let i = 1; i <= range; i++) { let index = lo - i if (index >= 0 && index < len && !exclude.has(ring[index][1])) { return [index, Math.abs(ring[index][0] - hash)] diff --git a/repos/effect/packages/effect/src/Iterable.ts b/repos/effect/packages/effect/src/Iterable.ts index 8529204a26..be5547629e 100644 --- a/repos/effect/packages/effect/src/Iterable.ts +++ b/repos/effect/packages/effect/src/Iterable.ts @@ -12,6 +12,7 @@ import type { NonEmptyArray } from "./Array.ts" import * as Equal from "./Equal.ts" import { dual } from "./Function.ts" +import * as Count from "./internal/count.ts" import * as InternalRecord from "./internal/record.ts" import type { Option } from "./Option.ts" import * as O from "./Option.ts" @@ -27,9 +28,9 @@ import type { NoInfer } from "./Types.ts" * * **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. + * The function is called with each index starting from `0`. If a length is + * provided, it is rounded down and normalized to at least `1`, with `NaN` + * treated as `1`. If no length is specified, the iterable is infinite. * * **Example** (Generating values by index) * @@ -56,7 +57,7 @@ import type { NoInfer } from "./Types.ts" 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 + const max = options?.length !== undefined ? Count.normalizeNonEmpty(options.length) : Infinity return { [Symbol.iterator]() { let i = 0 @@ -106,7 +107,8 @@ export const range = (start: number, end?: number): Iterable => { * * **Details** * - * `n` is normalized to an integer greater than or equal to `1`. + * `n` is rounded down and normalized to an integer greater than or equal to + * `1`. `NaN` is treated as `1`. * * **Example** (Repeating a value) * @@ -134,7 +136,8 @@ export const replicate: { * * **Details** * - * The result is lazy. Each repetition obtains a new iterator from `self`. + * The result is lazy. `n` is rounded down and normalized to at least `1`, with + * `NaN` treated as `1`. 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 @@ -533,7 +536,8 @@ export const headUnsafe = (self: Iterable): A => { * * **Details** * - * `n` is normalized to a non-negative integer. + * `n` is rounded down and normalized to a non-negative integer. `NaN` is + * treated as `0`. * * **Example** (Taking from the start) * @@ -564,21 +568,24 @@ export const headUnsafe = (self: Iterable): A => { 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() +} = dual(2, (self: Iterable, n: number): Iterable => { + const count = Count.normalize(n) + return { + [Symbol.iterator]() { + let i = 0 + const iterator = self[Symbol.iterator]() + return { + next() { + if (i < count) { + i++ + return iterator.next() + } + return { done: true, value: undefined } } - return { done: true, value: undefined } } } } -})) +}) /** * Takes the longest initial `Iterable` prefix for which all elements satisfy the @@ -641,7 +648,8 @@ export const takeWhile: { * * **Details** * - * `n` is normalized to a non-negative integer. + * `n` is rounded down and normalized to a non-negative integer. `NaN` is + * treated as `0`. * * **Example** (Dropping from the start) * @@ -671,24 +679,27 @@ export const takeWhile: { 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 } +} = dual(2, (self: Iterable, n: number): Iterable => { + const count = Count.normalize(n) + return { + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + while (i < count) { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + i++ } - i++ + return iterator.next() } - return iterator.next() } } } -})) +}) /** * Returns the first element that satisfies the specified @@ -1093,6 +1104,8 @@ export const contains: { /** * Splits an `Iterable` into length-`n` pieces. The last piece will be shorter if `n` does not evenly divide the length of * the `Iterable`. + * `n` is rounded down and normalized to at least `1`; `NaN` and non-positive + * values therefore produce singleton pieces. * * **Example** (Chunking an iterable) * @@ -1130,7 +1143,7 @@ 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)) + const safeN = Count.normalizeNonEmpty(n) return ({ [Symbol.iterator]() { let iterator: Iterator | undefined = self[Symbol.iterator]() diff --git a/repos/effect/packages/effect/src/JsonPatch.ts b/repos/effect/packages/effect/src/JsonPatch.ts index 605d24e2b9..8d0943dd38 100644 --- a/repos/effect/packages/effect/src/JsonPatch.ts +++ b/repos/effect/packages/effect/src/JsonPatch.ts @@ -307,8 +307,8 @@ function tokenize(pointer: string): Array { return pointer.split("/").slice(1).map(unescapeToken) } -/** Convert a reference token to a non-negative array index (rejects `-` and negatives). */ -function toIndex(token: string): number { +/** Converts a JSON Pointer reference token to a non-negative array index. */ +function toJsonPointerArrayIndex(token: string): number { if (!/^(0|[1-9]\d*)$/.test(token)) { throw new Error(`Invalid array index: "${token}"`) } @@ -332,7 +332,7 @@ function applyOperation(doc: Schema.Json, op: JsonPatchOperation): Schema.Json { if (lastToken === "-" && op.op !== "add") { throw new Error(`"-" is not valid for ${op.op} at "${op.path}".`) } - const index = lastToken === "-" ? parent.length : toIndex(lastToken) + const index = lastToken === "-" ? parent.length : toJsonPointerArrayIndex(lastToken) const maxIndex = op.op === "add" ? parent.length : parent.length - 1 if (index > maxIndex) throw new Error(`Array index out of bounds at "${op.path}".`) const updated = parent.slice() @@ -374,7 +374,7 @@ function resolveParent( const token = tokens[i] if (Array.isArray(cur)) { - const idx = toIndex(token) + const idx = toJsonPointerArrayIndex(token) if (idx >= cur.length) return null stack.push({ container: cur, token: idx }) cur = cur[idx] diff --git a/repos/effect/packages/effect/src/JsonPointer.ts b/repos/effect/packages/effect/src/JsonPointer.ts index 385d578b6b..78392b9da5 100644 --- a/repos/effect/packages/effect/src/JsonPointer.ts +++ b/repos/effect/packages/effect/src/JsonPointer.ts @@ -1,8 +1,8 @@ /** - * Helpers for escaping and unescaping JSON Pointer path segments. JSON Pointer - * uses `/` to separate path tokens inside a JSON document, so token text must - * encode literal `~` and `/` characters. This module provides the two RFC 6901 - * token conversions used by JSON Patch and related path handling. + * Helpers for escaping JSON Pointer path segments and converting JSON Pointer + * URI fragments. JSON Pointer uses `/` to separate path tokens inside a JSON + * document, so token text must encode literal `~` and `/` characters. URI + * fragments additionally apply percent-encoding after JSON Pointer escaping. * * @since 4.0.0 */ @@ -78,3 +78,96 @@ export function escapeToken(token: string): string { export function unescapeToken(token: string): string { return token.replace(/~1/g, "/").replace(/~0/g, "~") } + +/** @internal */ +export function formatUriFragmentToken(token: string): string { + return encodeURI(escapeToken(token)).replace(/#/g, "%23") +} + +/** @internal */ +export function decodeUriFragment(fragment: string): string | undefined { + if (fragment.length === 0 || fragment === "#") return "" + if (!fragment.startsWith("#")) return undefined + const encoded = fragment.slice(1) + try { + if (encodeURI(encoded).replace(/%25/g, "%").replace(/#/g, "%23") !== encoded) return undefined + const pointer = decodeURIComponent(encoded) + return pointer.startsWith("/") && !/~(?:[^01]|$)/.test(pointer) ? pointer : undefined + } catch { + return undefined + } +} + +/** + * Parses a JSON Pointer URI fragment into decoded path tokens. + * + * **When to use** + * + * Use when you need to resolve a URI fragment against a JSON document. + * + * **Details** + * + * Percent-encoding is decoded before the pointer is split into tokens, then + * each token is decoded with {@link unescapeToken}. The empty string and `#` + * both represent the document root. + * + * **Gotchas** + * + * Returns `undefined` when the input is not a URI fragment, contains characters + * that require percent-encoding, or contains an invalid JSON Pointer escape + * sequence. + * + * **Example** (Parsing URI fragments) + * + * ```ts import.meta.vitest + * import { JsonPointer } from "effect" + * + * JsonPointer.parseUriFragment("#/users/a~1b") // => ["users", "a/b"] + * JsonPointer.parseUriFragment("#/caf%C3%A9") // => ["café"] + * JsonPointer.parseUriFragment("#/%") // => undefined + * JsonPointer.parseUriFragment("#/a#b") // => undefined + * ``` + * + * @see {@link formatUriFragment} for the inverse operation + * @category decoding + * @since 4.0.0 + */ +export function parseUriFragment(fragment: string): ReadonlyArray | undefined { + const pointer = decodeUriFragment(fragment) + return pointer === undefined ? undefined : pointer.length === 0 ? [] : pointer.slice(1).split("/").map(unescapeToken) +} + +/** + * Formats path tokens as a JSON Pointer URI fragment. + * + * **When to use** + * + * Use when you need a URI fragment that identifies a value in a JSON document. + * + * **Details** + * + * Each token is encoded with {@link escapeToken} before URI percent-encoding + * is applied. An empty path is formatted as `#`. + * + * **Gotchas** + * + * Throws a `URIError` when a token contains an unpaired surrogate. + * + * **Example** (Formatting a URI fragment) + * + * ```ts import.meta.vitest + * import { JsonPointer } from "effect" + * + * JsonPointer.formatUriFragment(["users", "a/b", "Rate%"]) // => "#/users/a~1b/Rate%25" + * ``` + * + * @see {@link parseUriFragment} for the inverse operation + * @category encoding + * @since 4.0.0 + */ +export function formatUriFragment(path: ReadonlyArray): string { + return path.reduce( + (fragment, token) => `${fragment}/${formatUriFragmentToken(token)}`, + "#" + ) +} diff --git a/repos/effect/packages/effect/src/JsonSchema.ts b/repos/effect/packages/effect/src/JsonSchema.ts index 054b8e6b65..5cbd698379 100644 --- a/repos/effect/packages/effect/src/JsonSchema.ts +++ b/repos/effect/packages/effect/src/JsonSchema.ts @@ -9,7 +9,7 @@ * @since 4.0.0 */ import * as InternalRecord from "./internal/record.ts" -import { escapeToken, unescapeToken } from "./JsonPointer.ts" +import { formatUriFragment, parseUriFragment } from "./JsonPointer.ts" import * as Predicate from "./Predicate.ts" /** @@ -218,9 +218,9 @@ function isMetaSchemaUri(value: unknown, uri: string): boolean { } function rewriteOpenApiComponentsReference(reference: string): string { - const path = reference.startsWith("#") ? parsePointerFragment(reference) : undefined + const path = reference.startsWith("#") ? parseUriFragment(reference) : undefined return path !== undefined && path[0] === "components" && path[1] === "schemas" - ? formatPointerFragment(["$defs", ...path.slice(2)]) + ? formatUriFragment(["$defs", ...path.slice(2)]) : reference } @@ -627,7 +627,7 @@ export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-20 return transformSchema(schema, (schema, inEmbeddedResource) => { rejectKeywordCollisions(schema, OPEN_API_31_TARGET_COLLISIONS, "OpenAPI 3.1", "Draft 2020-12") rewriteSchemaRef(schema, (reference, keyword) => { - const path = reference.startsWith("#") ? parsePointerFragment(reference) : undefined + const path = reference.startsWith("#") ? parseUriFragment(reference) : undefined if (path === undefined || path[0] !== "$defs" || path.length < 2) return reference const key = path[1] if (isRootResource) { @@ -643,7 +643,7 @@ export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-20 } return inEmbeddedResource ? reference - : formatPointerFragment(["components", "schemas", keyMap.get(key) ?? key, ...path.slice(2)]) + : formatUriFragment(["components", "schemas", keyMap.get(key) ?? key, ...path.slice(2)]) }) }) as JsonSchema } @@ -682,7 +682,7 @@ export function sanitizeOpenApiComponentsSchemasKey(s: string): string { /** @internal */ export function getReferenceKey($ref: string): string | undefined { - const path = $ref.startsWith("#") ? parsePointerFragment($ref) : undefined + const path = $ref.startsWith("#") ? parseUriFragment($ref) : undefined return path !== undefined && path.length === 2 && path[0] === "$defs" ? path[1] : undefined @@ -815,7 +815,7 @@ function runConverter(adapter: Adapter, options: ConverterOptions | undefined let reference = value const resolved = resolveUrl(value, sourceResource) if (resolved !== undefined) { - const sourcePointer = parsePointerFragment(resolved.hash) + const sourcePointer = parseUriFragment(resolved.hash) resolved.hash = "" if (sourcePointer !== undefined) { const targetPath = locations.get(locationKey(resolved.href, sourcePointer)) @@ -927,29 +927,11 @@ function resolveResourceUri(value: unknown, base: string): string | undefined { return url.href } -function parsePointerFragment(hash: string): Path | undefined { - if (hash.length === 0) return [] - let pointer: string - try { - pointer = decodeURIComponent(hash.slice(1)) - } catch { - return undefined - } - if (!pointer.startsWith("/")) return undefined - return /~(?:[^01]|$)/.test(pointer) ? undefined : pointer.slice(1).split("/").map(unescapeToken) -} - function relocateReference(reference: string, targetPath: Path): string { const index = reference.indexOf("#") if (index === -1 && targetPath.length === 0) return reference const uri = index === -1 ? reference : reference.slice(0, index) - return `${uri}${formatPointerFragment(targetPath)}` -} - -function formatPointerFragment(path: Path): string { - return path.length === 0 - ? "#" - : `#/${path.map((token) => encodeURI(escapeToken(token)).replace(/#/g, "%23")).join("/")}` + return `${uri}${formatUriFragment(targetPath)}` } function locationKey(resource: string, pointer: Path): string { diff --git a/repos/effect/packages/effect/src/Layer.ts b/repos/effect/packages/effect/src/Layer.ts index ede9373c18..c5bd8f6292 100644 --- a/repos/effect/packages/effect/src/Layer.ts +++ b/repos/effect/packages/effect/src/Layer.ts @@ -1137,6 +1137,8 @@ export const effectDiscard = (effect: Effect): Layer(evaluate: LazyArg>): Layer => fromBuildMemo((memoMap, scope) => internalEffect.suspend(() => evaluate().build(memoMap, scope))) +const unwrapKey = Context.Service>("effect/Layer/unwrap") + /** * Unwraps a `Layer` from an `Effect`, flattening the nested structure. * @@ -1173,10 +1175,7 @@ export const suspend = (evaluate: LazyArg>): Layer( self: Effect, E, R> -): Layer> => { - const service = Context.Service>("effect/Layer/unwrap") - return flatMap(effect(service)(self), Context.get(service)) -} +): Layer> => flatMap(effect(unwrapKey)(self), Context.get(unwrapKey)) const mergeAllEffect = , ...Array>]>( layers: Layers, @@ -1738,21 +1737,24 @@ export const tap: { * @since 2.0.0 */ export const tapError: { - ( - f: (e: XE) => Effect + ( + f: (e: Types.NoInfer) => Effect ): (self: Layer) => Layer> - ( + ( + f: (e: E) => Effect + ): (self: Layer) => Layer> + ( self: Layer, - f: (e: XE) => Effect + f: (e: Types.NoInfer) => Effect ): Layer> -} = dual(2, ( +} = dual(2, ( self: Layer, - f: (e: XE) => Effect + f: (e: E) => Effect ): Layer> => fromBuild((memoMap, scope) => internalEffect.catch_( self.build(memoMap, scope), - (error) => Scope.provide(internalEffect.andThen(f(error as XE), internalEffect.fail(error)), scope) + (error) => Scope.provide(internalEffect.andThen(f(error), internalEffect.fail(error)), scope) ) )) @@ -1778,22 +1780,24 @@ export const tapError: { * @since 4.0.0 */ export const tapCause: { - ( - f: (cause: Cause.Cause) => Effect + ( + f: (cause: Cause.Cause>) => Effect ): (self: Layer) => Layer> - ( + ( + f: (cause: Cause.Cause) => Effect + ): (self: Layer) => Layer> + ( self: Layer, - f: (cause: Cause.Cause) => Effect + f: (cause: Cause.Cause>) => Effect ): Layer> -} = dual(2, ( +} = dual(2, ( self: Layer, - f: (cause: Cause.Cause) => Effect + f: (cause: Cause.Cause) => Effect ): Layer> => fromBuild((memoMap, scope) => internalEffect.catchCause( self.build(memoMap, scope), - (cause) => - Scope.provide(internalEffect.andThen(f(cause as Cause.Cause), internalEffect.failCause(cause)), scope) + (cause) => Scope.provide(internalEffect.andThen(f(cause), internalEffect.failCause(cause)), scope) ) )) @@ -2676,7 +2680,7 @@ export const withSpan: { (span) => internalEffect.addFinalizer((exit) => options.onEnd!(span, exit)) ) : internalEffect.makeSpanScoped(name, options), - (span) => withParentSpan(self, span) + (span) => withParentSpan(self, span, options) ) ) } @@ -2689,7 +2693,7 @@ export const withSpan: { (span) => internalEffect.addFinalizer((exit) => options.onEnd!(span, exit)) ) : internalEffect.makeSpanScoped(name, options), - (span) => withParentSpan(self, span) + (span) => withParentSpan(self, span, options) ) ) } as any diff --git a/repos/effect/packages/effect/src/LayerMap.ts b/repos/effect/packages/effect/src/LayerMap.ts index eabd4ec59b..d9bc31fe03 100644 --- a/repos/effect/packages/effect/src/LayerMap.ts +++ b/repos/effect/packages/effect/src/LayerMap.ts @@ -423,7 +423,7 @@ export const Service = () => : Options extends { readonly layers: infer Layers } ? keyof Layers : never, Service.Success, - Options extends { readonly preload: true } ? never : Service.Error, + Service.Error, Service.Services, Options extends { readonly preload: true } ? Service.Error : Options extends { readonly preloadKeys: Iterable } ? Service.Error @@ -431,11 +431,13 @@ export const Service = () => Options extends { readonly dependencies: ReadonlyArray> } ? Options["dependencies"][number] : never > => { - const Err = globalThis.Error as any const limit = getStackTraceLimit() - setStackTraceLimit(2) - const creationError = new Err() - setStackTraceLimit(limit) + let creationError: Error | undefined + if (limit !== 0) { + setStackTraceLimit(2) + creationError = new globalThis.Error() + setStackTraceLimit(limit) + } function TagClass() {} const TagClass_ = TagClass as any as Mutable> @@ -443,7 +445,7 @@ export const Service = () => TagClass.key = id Object.defineProperty(TagClass, "stack", { get() { - return creationError.stack + return creationError?.stack } }) diff --git a/repos/effect/packages/effect/src/LayerRef.ts b/repos/effect/packages/effect/src/LayerRef.ts index a961007733..4d18aeb858 100644 --- a/repos/effect/packages/effect/src/LayerRef.ts +++ b/repos/effect/packages/effect/src/LayerRef.ts @@ -349,11 +349,13 @@ export const Service = () => [Preload] extends [true] ? E : never, Deps[number] > => { - const Err = globalThis.Error as any const limit = getStackTraceLimit() - setStackTraceLimit(2) - const creationError = new Err() - setStackTraceLimit(limit) + let creationError: Error | undefined + if (limit !== 0) { + setStackTraceLimit(2) + creationError = new globalThis.Error() + setStackTraceLimit(limit) + } function TagClass() {} const TagClass_ = TagClass as any as Mutable> @@ -361,7 +363,7 @@ export const Service = () => TagClass.key = id Object.defineProperty(TagClass, "stack", { get() { - return creationError.stack + return creationError?.stack } }) diff --git a/repos/effect/packages/effect/src/Logger.ts b/repos/effect/packages/effect/src/Logger.ts index 168a53870d..280febf2ca 100644 --- a/repos/effect/packages/effect/src/Logger.ts +++ b/repos/effect/packages/effect/src/Logger.ts @@ -752,7 +752,9 @@ export const batched = dual< /** * A `Logger` which outputs logs in a "pretty" format and writes them to the - * console. + * console. Chooses between tty and browser implementation. If the runtime + * platform is known and fixed, prefer {@link consolePrettyBrowser} or + * {@link consolePrettyTty}. * * **Details** * @@ -763,24 +765,135 @@ export const batched = dual< * **Example** (Logging with pretty console output) * * ```ts import.meta.vitest - * import { Logger } from "effect" + * import { Effect, Logger } from "effect" + * + * const prettyLogger = Logger.layer([Logger.consolePretty()]) + * + * Effect.log("hello").pipe( + * Effect.withLogSpan('label'), + * Effect.annotateLogs('key', 'value'), + * Effect.provide(prettyLogger), + * Effect.runSync + * ) + * ``` + * + * **Example** (Logging with console.error, when the environment has TTY) + * + * ```ts import.meta.vitest + * import { Effect, Layer, Logger } from "effect" * - * const prettyLogger = Logger.consolePretty({ colors: false }) - * Logger.isLogger(prettyLogger) // => true + * const prettyLoggerLayer = Layer.merge( + * Logger.layer([Logger.consolePretty()]), + * Layer.succeed(Logger.LogToStderr, true) + * ) + * + * Effect.log('hello').pipe( + * Effect.provide(prettyLoggerLayer), + * Effect.runSync + * ) * ``` * * @category constructors + * @see {@link consolePrettyBrowser} for browser-specific implementation + * @see {@link consolePrettyTty} for the TTY-mode implementation * @since 4.0.0 */ export const consolePretty: ( options?: { readonly colors?: "auto" | boolean | undefined - readonly stderr?: boolean | undefined readonly formatDate?: ((date: Date) => string) | undefined readonly mode?: "browser" | "tty" | "auto" | undefined } ) => Logger = effect.consolePretty +/** + * A `Logger` which outputs logs in a "pretty" format and writes them to the + * console. Intended to be used on platforms with a browser console. + * + * **Details** + * + * For example, pretty output can render as + * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line + * such as `key: value`. + * + * **Example** (Logging with pretty console output) + * + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * + * const prettyLogger = Logger.layer([Logger.consolePrettyBrowser()]) + * + * Effect.log("hello").pipe( + * Effect.withLogSpan('label'), + * Effect.annotateLogs('key', 'value'), + * Effect.provide(prettyLogger), + * Effect.runSync + * ) + * ``` + * + * @category constructors + * @see {@link consolePretty} for the platform-independent implementation + * @since 4.0.0 + */ +export const consolePrettyBrowser: ( + options?: { + readonly colors?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + } +) => Logger = effect.prettyLoggerBrowser + +/** + * A `Logger` which outputs logs in a "pretty" format and writes them to the + * console. Intended to be used on platforms with tty console. + * + * **Details** + * + * For example, pretty output can render as + * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line + * such as `key: value`. + * + * **Example** (Logging with pretty console output) + * + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * + * const prettyLogger = Logger.layer([Logger.consolePrettyTty()]) + * + * Effect.log("hello").pipe( + * Effect.withLogSpan('label'), + * Effect.annotateLogs('key', 'value'), + * Effect.provide(prettyLogger), + * Effect.runSync + * ) + * ``` + * + * **Example** (Logging with console.error) + * + * ```ts import.meta.vitest + * import { Effect, Layer, Logger } from "effect" + * + * const prettyLoggerLayer = Layer.merge( + * Logger.layer([Logger.consolePrettyTty()]), + * Layer.succeed(Logger.LogToStderr, true) + * ) + * + * Effect.log('hello').pipe( + * Effect.provide(prettyLoggerLayer), + * Effect.runSync + * ) + * ``` + * + * @category constructors + * @see {@link consolePretty} for the platform-independent implementation + * @since 4.0.0 + */ +export const consolePrettyTty: ( + options?: { + readonly colors?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + } +) => Logger = effect.prettyLoggerTty + /** * A `Logger` which outputs logs using the [logfmt](https://brandur.org/logfmt) * style and writes them to the console. @@ -951,9 +1064,8 @@ export const layer = < * * const writes: Array = [] * const file = { - * write: (buffer: Uint8Array) => Effect.sync(() => { + * writeAll: (buffer: Uint8Array) => Effect.sync(() => { * writes.push(new TextDecoder().decode(buffer).trim()) - * return FileSystem.Size(buffer.length) * }) * } as unknown as FileSystem.File * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) }) @@ -977,9 +1089,8 @@ export const layer = < * * const writes: Array = [] * const file = { - * write: (buffer: Uint8Array) => Effect.sync(() => { + * writeAll: (buffer: Uint8Array) => Effect.sync(() => { * writes.push(new TextDecoder().decode(buffer).trim()) - * return FileSystem.Size(buffer.length) * }) * } as unknown as FileSystem.File * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) }) @@ -1030,7 +1141,7 @@ export const toFile = dual< const encoder = new TextEncoder() return yield* batched(self, { window: options?.batchWindow ?? 1000, - flush: (output) => effect.ignore(logFile.write(encoder.encode(output.join("\n") + "\n"))) + flush: (output) => effect.ignore(logFile.writeAll(encoder.encode(output.join("\n") + "\n"))) }) }) ) diff --git a/repos/effect/packages/effect/src/ManagedRuntime.ts b/repos/effect/packages/effect/src/ManagedRuntime.ts index 90117459ff..e0a99849da 100644 --- a/repos/effect/packages/effect/src/ManagedRuntime.ts +++ b/repos/effect/packages/effect/src/ManagedRuntime.ts @@ -317,7 +317,7 @@ export const make = ( self.cachedContext = context }) ), - { ...defaultRunOptions, scheduler: fiber.currentScheduler } + { ...defaultRunOptions, scheduler: fiber.cache.scheduler } ) } return Effect.flatten(Fiber.await(buildFiber)) diff --git a/repos/effect/packages/effect/src/Match.ts b/repos/effect/packages/effect/src/Match.ts index 510da5ef69..918c44d209 100644 --- a/repos/effect/packages/effect/src/Match.ts +++ b/repos/effect/packages/effect/src/Match.ts @@ -20,6 +20,22 @@ import type { Unify } from "./Unify.ts" const TypeId = internal.TypeId +// The conditional must stay deferred until P is inferred. Replacing it with an +// intersection loses contextual typing for nested generic calls (microsoft/TypeScript#52864). +type Contextual = internal.Contextual + +type TagHandlers = { + readonly [Tag in Types.Tags & string]: (_: Extract>) => Ret +} + +type PartialTagHandlers = { + readonly [Tag in Types.Tags & string]?: ((_: Extract>) => Ret) | undefined +} + +type ValueTagHandlers = { + readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract) => any +} + /** * Marker used by `Matcher` to distinguish matchers created with `Match.value`. * @@ -316,10 +332,13 @@ export const type: () => Matcher, I, never, never> = /** * Creates a reusable matcher from a function that selects the value to match. * + * **Details** + * * The compiled matcher keeps the selector's original argument list. Case * handlers receive the narrowed selected value followed by those arguments. * - * @example + * **Example** (Creating a reusable matcher) + * * ```ts import.meta.vitest * import { Match } from "effect" * @@ -419,15 +438,20 @@ export const valueTags: { < const I, P extends - & { readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract) => any } + & ValueTagHandlers & { readonly [Tag in Exclude>]: never } - >(fields: P): (input: I) => Unify> + >( + fields: Contextual> + ): (input: I) => Unify> < const I, P extends - & { readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract) => any } + & ValueTagHandlers & { readonly [Tag in Exclude>]: never } - >(input: I, fields: P): Unify> + >( + input: I, + fields: Contextual> + ): Unify> } = internal.valueTags /** @@ -880,10 +904,10 @@ export const discriminators: ( R, Ret, P extends - & { readonly [Tag in Types.Tags & string]?: ((_: Extract>) => Ret) | undefined } + & PartialTagHandlers & { readonly [Tag in Exclude>]: never } >( - fields: P + fields: Contextual> ) => ( self: Matcher ) => Matcher< @@ -943,10 +967,10 @@ export const discriminatorsExhaustive: ( R, Ret, P extends - & { readonly [Tag in Types.Tags & string]: (_: Extract>) => Ret } + & TagHandlers & { readonly [Tag in Exclude>]: never } >( - fields: P + fields: Contextual> ) => ( self: Matcher ) => [Pr] extends [never] ? (u: I) => Unify> : Unify> = @@ -1103,10 +1127,10 @@ export const tags: < R, Ret, P extends - & { readonly [Tag in Types.Tags<"_tag", R> & string]?: ((_: Extract>) => Ret) | undefined } + & PartialTagHandlers<"_tag", R, Ret> & { readonly [Tag in Exclude>]: never } >( - fields: P + fields: Contextual> ) => ( self: Matcher ) => Matcher< @@ -1158,10 +1182,10 @@ export const tagsExhaustive: < R, Ret, P extends - & { readonly [Tag in Types.Tags<"_tag", R> & string]: (_: Extract>) => Ret } + & TagHandlers<"_tag", R, Ret> & { readonly [Tag in Exclude>]: never } >( - fields: P + fields: Contextual> ) => ( self: Matcher ) => [Pr] extends [never] ? (u: I) => Unify> : Unify> = @@ -2051,7 +2075,7 @@ export const exhaustive: >( ) => [Pr] extends [never] ? [Args] extends [[]] ? (u: I) => Unify : (...args: Args) => Unify : Unify = internal.exhaustive -const SafeRefinementId = "~effect/match/Match/SafeRefinement" +const SafeRefinementId = "~effect/Match/SafeRefinement" /** * A safe refinement that narrows types without runtime errors. diff --git a/repos/effect/packages/effect/src/Metric.ts b/repos/effect/packages/effect/src/Metric.ts index adbf13db79..14eb02edc8 100644 --- a/repos/effect/packages/effect/src/Metric.ts +++ b/repos/effect/packages/effect/src/Metric.ts @@ -1609,7 +1609,15 @@ export const CurrentMetricAttributes = Context.Reference(Cu defaultValue: () => ({}) }) -const MetricRegistryKey = "~effect/observability/Metric/MetricRegistryKey" +const MetricRegistryKey = "effect/Metric/MetricRegistry" + +/** + * The registry used to store metric metadata and hooks. + * + * @category services + * @since 4.0.0 + */ +export type MetricRegistry = Map> /** * Context reference for the metric registry in the current context. @@ -1637,12 +1645,12 @@ const MetricRegistryKey = "~effect/observability/Metric/MetricRegistryKey" * @category services * @since 4.0.0 */ -export const MetricRegistry = Context.Reference>>( +export const MetricRegistry: Context.Reference = Context.Reference( MetricRegistryKey, { defaultValue: () => new Map() } ) -const TypeId = "~effect/observability/Metric" +const TypeId = "~effect/Metric" abstract class Metric$ implements Metric { readonly [TypeId] = TypeId @@ -1652,8 +1660,7 @@ abstract class Metric$ implements Metric { declare readonly Input: Contravariant declare readonly State: Covariant - readonly #metadataCache = new WeakMap>() - #metadata: Metric.Metadata | undefined + readonly #metadata = new WeakMap>() readonly id: string readonly description: string | undefined @@ -1685,20 +1692,15 @@ abstract class Metric$ implements Metric { hook(context: Context.Context): Metric.Hooks { const extraAttributes = Context.get(context, CurrentMetricAttributes) - if (Object.keys(extraAttributes).length === 0) { - if (Predicate.isNotUndefined(this.#metadata)) { - return this.#metadata.hooks - } - this.#metadata = this.getOrCreate(context, this.attributes) - return this.#metadata.hooks + if (Object.keys(extraAttributes).length > 0) { + return this.getOrCreate(context, mergeAttributes(this.attributes, extraAttributes)).hooks } - const mergedAttributes = mergeAttributes(this.attributes, extraAttributes) - let metadata = this.#metadataCache.get(mergedAttributes) - if (Predicate.isNotUndefined(metadata)) { - return metadata.hooks + const registry = Context.get(context, MetricRegistry) + let metadata = this.#metadata.get(registry) + if (Predicate.isUndefined(metadata)) { + metadata = this.getOrCreate(context, this.attributes) + this.#metadata.set(registry, metadata) } - metadata = this.getOrCreate(context, mergedAttributes) - this.#metadataCache.set(mergedAttributes, metadata) return metadata.hooks } @@ -3235,14 +3237,13 @@ const fiberFailures = counter("child_fiber_failures", { * ```ts import.meta.vitest * import { Metric } from "effect" * - * Metric.FiberRuntimeMetricsKey // => "effect/observability/Metric/FiberRuntimeMetricsKey" + * Metric.FiberRuntimeMetricsKey // => "effect/Metric/FiberRuntimeMetrics" * ``` * * @category constants * @since 4.0.0 */ -export const FiberRuntimeMetricsKey: "effect/observability/Metric/FiberRuntimeMetricsKey" = - InternalMetric.FiberRuntimeMetricsKey +export const FiberRuntimeMetricsKey: "effect/Metric/FiberRuntimeMetrics" = InternalMetric.FiberRuntimeMetricsKey /** * Interface for the fiber runtime metrics service that tracks fiber lifecycle events. @@ -3496,7 +3497,9 @@ function makeHooks( } function serializeAttributes(attributes: Metric.Attributes): string { - return JSON.stringify(Array.isArray(attributes) ? attributes : Object.entries(attributes)) + const entries = Array.isArray(attributes) ? [...attributes] : Object.entries(attributes) + entries.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0) + return JSON.stringify(entries) } function mergeAttributes( diff --git a/repos/effect/packages/effect/src/MutableHashMap.ts b/repos/effect/packages/effect/src/MutableHashMap.ts index c95c6309cb..a9d97d7478 100644 --- a/repos/effect/packages/effect/src/MutableHashMap.ts +++ b/repos/effect/packages/effect/src/MutableHashMap.ts @@ -20,7 +20,7 @@ import type { Pipeable } from "./Pipeable.ts" import { pipeArguments } from "./Pipeable.ts" import { hasProperty } from "./Predicate.ts" -const TypeId = "~effect/collections/MutableHashMap" +const TypeId = "~effect/MutableHashMap" /** * A mutable hash map that stores key-value pairs and supports both referential diff --git a/repos/effect/packages/effect/src/MutableHashSet.ts b/repos/effect/packages/effect/src/MutableHashSet.ts index 3b1965c9bc..654f7c6338 100644 --- a/repos/effect/packages/effect/src/MutableHashSet.ts +++ b/repos/effect/packages/effect/src/MutableHashSet.ts @@ -17,7 +17,7 @@ import type { Pipeable } from "./Pipeable.ts" import { pipeArguments } from "./Pipeable.ts" import { hasProperty } from "./Predicate.ts" -const TypeId = "~effect/collections/MutableHashSet" +const TypeId = "~effect/MutableHashSet" /** * A mutable hash set for storing unique values with Effect structural equality diff --git a/repos/effect/packages/effect/src/MutableList.ts b/repos/effect/packages/effect/src/MutableList.ts index e40106a5e4..3506009e81 100644 --- a/repos/effect/packages/effect/src/MutableList.ts +++ b/repos/effect/packages/effect/src/MutableList.ts @@ -9,6 +9,7 @@ * @since 4.0.0 */ import * as Arr from "./Array.ts" +import * as Count from "./internal/count.ts" /** * A mutable linked list data structure optimized for high-throughput operations. @@ -290,6 +291,7 @@ export const prependAllUnsafe = (self: MutableList, messages: ReadonlyArra offset: 0, next: self.head } + if (!self.tail && messages.length > 0) self.tail = self.head self.length += self.head.array.length } @@ -399,6 +401,11 @@ export const clear = (self: MutableList): void => { * The taken elements are removed from the list. This operation is optimized for performance * and includes zero-copy optimizations when possible. * + * **Details** + * + * Finite fractional values of `n` are rounded down. `NaN` and non-positive + * values leave the list unchanged and return an empty array. + * * **Example** (Taking batches) * * ```ts import.meta.vitest @@ -416,6 +423,7 @@ export const clear = (self: MutableList): void => { * @since 4.0.0 */ export const takeN = (self: MutableList, n: number): Array => { + n = Count.normalize(n) if (n <= 0 || !self.head) return [] n = Math.min(n, self.length) if (n === self.length && self.head?.offset === 0 && !self.head.next) { @@ -455,8 +463,9 @@ export const takeN = (self: MutableList, n: number): Array => { * * **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 + * Finite fractional values of `n` are rounded down. If `n` is `NaN` or + * non-positive, or the list is empty, the list is left unchanged. If the + * normalized count 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 @@ -466,6 +475,7 @@ export const takeN = (self: MutableList, n: number): Array => { * @since 4.0.0 */ export const takeNVoid = (self: MutableList, n: number): void => { + n = Count.normalize(n) if (n <= 0 || !self.head) return n = Math.min(n, self.length) if (n === self.length && self.head?.offset === 0 && !self.head.next) { @@ -556,12 +566,18 @@ export const take = (self: MutableList): Empty | A => { * Use when you need to inspect or snapshot a bounded prefix of the list without * consuming it. * + * **Details** + * + * Finite fractional values of `n` are rounded down. `NaN` and non-positive + * values return an empty array. + * * @see {@link takeN} for removing up to `n` values and returning them as an array * * @category converting * @since 4.0.0 */ export const toArrayN = (self: MutableList, n: number): Array => { + n = Count.normalize(n) if (n <= 0) return [] const length = Math.min(n, self.length) const out = new Array(length) diff --git a/repos/effect/packages/effect/src/Number.ts b/repos/effect/packages/effect/src/Number.ts index 89194befdf..b42e1c07ec 100644 --- a/repos/effect/packages/effect/src/Number.ts +++ b/repos/effect/packages/effect/src/Number.ts @@ -441,6 +441,8 @@ export const between: { * - 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`. + * - `NaN` is ordered below every non-`NaN` number by `Number.Order`, so it is + * clamped to `minimum`. * * **Example** (Clamping to a range) * @@ -641,7 +643,7 @@ export const remainder: { const selfDecCount = (selfString.split(".")[1] || "").length const divisorDecCount = (divisorString.split(".")[1] || "").length const decCount = selfDecCount > divisorDecCount ? selfDecCount : divisorDecCount - const selfInt = parseInt(self.toFixed(decCount).replace(".", "")) + const selfInt = self === 0 ? self : parseInt(self.toFixed(decCount).replace(".", "")) const divisorInt = parseInt(divisor.toFixed(decCount).replace(".", "")) return (selfInt % divisorInt) / Math.pow(10, decCount) }) diff --git a/repos/effect/packages/effect/src/Optic.ts b/repos/effect/packages/effect/src/Optic.ts index f132e6617a..c21149a6f1 100644 --- a/repos/effect/packages/effect/src/Optic.ts +++ b/repos/effect/packages/effect/src/Optic.ts @@ -12,6 +12,7 @@ * @since 4.0.0 */ +import * as Arr from "./Array.ts" import { dual, identity } from "./Function.ts" import * as InternalRecord from "./internal/record.ts" import * as Option from "./Option.ts" @@ -1001,8 +1002,11 @@ class OptionalImpl implements Optional { (a, s) => { const copy = cloneShallow(s) if (a === undefined) { - if (Array.isArray(copy) && typeof key === "number") { - copy.splice(key, 1) + if ( + Array.isArray(copy) && + (typeof key === "number" || (typeof key === "string" && Arr.isCanonicalArrayIndex(key))) + ) { + copy.splice(Number(key), 1) } else { delete copy[key] } @@ -1058,10 +1062,10 @@ class OptionalImpl implements Optional { ) } pick(keys: any) { - return this.compose(makeLens(Struct.pick(keys), (p, a) => ({ ...a, ...p }))) + return this.compose(makeLens(Struct.pick(keys), (p, a) => ({ ...Struct.omit(a, keys), ...p }))) } omit(keys: any) { - return this.compose(makeLens(Struct.omit(keys), (o, a) => ({ ...a, ...o }))) + return this.compose(makeLens(Struct.omit(keys), (o, a) => ({ ...Struct.pick(a, keys), ...o }))) } notUndefined(): any { return this.refine(Predicate.isNotUndefined, { expected: "a value other than `undefined`" }) diff --git a/repos/effect/packages/effect/src/Option.ts b/repos/effect/packages/effect/src/Option.ts index 9e94f85626..3e497eb10d 100644 --- a/repos/effect/packages/effect/src/Option.ts +++ b/repos/effect/packages/effect/src/Option.ts @@ -33,7 +33,7 @@ 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" +const TypeId = "~effect/Option" /** * The `Option` data type represents optional values. An `Option` is either diff --git a/repos/effect/packages/effect/src/Order.ts b/repos/effect/packages/effect/src/Order.ts index c52ea36044..211b44bbda 100644 --- a/repos/effect/packages/effect/src/Order.ts +++ b/repos/effect/packages/effect/src/Order.ts @@ -368,6 +368,7 @@ export function alwaysEqual(): Order { * * Applies orders in iteration order and short-circuits on the first non-zero * result. It returns `0` only if all orders return `0`. + * The collection is materialized when the order is created, so it must be finite. * * **Example** (Combining multiple Orders) * @@ -397,9 +398,10 @@ export function alwaysEqual(): Order { * @since 2.0.0 */ export function combineAll(collection: Iterable>): Order { + const orders = Array.from(collection) return make((a1, a2) => { let out: Ordering = 0 - for (const O of collection) { + for (const O of orders) { out = O(a1, a2) if (out !== 0) { return out diff --git a/repos/effect/packages/effect/src/PartitionedSemaphore.ts b/repos/effect/packages/effect/src/PartitionedSemaphore.ts index d700e46eeb..07aaabfb6f 100644 --- a/repos/effect/packages/effect/src/PartitionedSemaphore.ts +++ b/repos/effect/packages/effect/src/PartitionedSemaphore.ts @@ -216,8 +216,7 @@ export const makeUnsafe = (options: { } const cleanup = () => { - waiters.delete(entry) - if (waiters.size === 0) { + if (waiters.delete(entry) && waiters.size === 0) { MutableHashMap.remove(partitions, key) } } diff --git a/repos/effect/packages/effect/src/Path.ts b/repos/effect/packages/effect/src/Path.ts index 13195b6c2c..946b18f0a0 100644 --- a/repos/effect/packages/effect/src/Path.ts +++ b/repos/effect/packages/effect/src/Path.ts @@ -29,7 +29,7 @@ import { BadArgument } from "./PlatformError.ts" * @category type IDs * @since 4.0.0 */ -export const TypeId = "~effect/platform/Path" +export const TypeId = "~effect/Path" /** * Defines the service interface for platform-specific path manipulation. diff --git a/repos/effect/packages/effect/src/PlatformError.ts b/repos/effect/packages/effect/src/PlatformError.ts index 25ae0dc9ed..b2fca10c2b 100644 --- a/repos/effect/packages/effect/src/PlatformError.ts +++ b/repos/effect/packages/effect/src/PlatformError.ts @@ -11,7 +11,7 @@ */ import * as Data from "./Data.ts" -const TypeId = "~effect/platform/PlatformError" +const TypeId = "~effect/PlatformError" /** * Error data for an invalid argument passed to a platform API. diff --git a/repos/effect/packages/effect/src/Pool.ts b/repos/effect/packages/effect/src/Pool.ts index 49294db98d..104c0224f9 100644 --- a/repos/effect/packages/effect/src/Pool.ts +++ b/repos/effect/packages/effect/src/Pool.ts @@ -623,8 +623,13 @@ const releaseItem = (self: Pool, item: PoolItem): Effect.Effec if (state.invalidated.has(item)) { return invalidatePoolItem(self, item) } - if (item.refCount === self.config.concurrency - 1) { - addAvailable(self, item) + // Every release frees one slot, so it can admit one waiter. Reacting only + // to the saturated-to-unsaturated transition strands the rest: several + // leases returning at once would wake a single waiter and leave the others + // asleep against an item that has capacity for them. `addAvailable` is + // idempotent, so re-adding an available item is free. + if (item.refCount < self.config.concurrency) { + addAvailableFront(self, item) wakeWaiters(self, fiber, 1) } return internal.void @@ -668,8 +673,12 @@ const wakeAll = (self: Pool): Effect.Effect => return internal.void }) +// Reservations prevent reuse without extending the lifetime of borrowed items. +const reservations = new WeakMap, number>() + +/** Adds a freshly acquired item, which has no use behind it, at the back. */ const addAvailable = (self: Pool, item: PoolItem): void => { - if (item.isAvailable) return + if (item.isAvailable || reservations.has(item)) return item.isAvailable = true item.availablePrevious = self.state.availableTail item.availableNext = undefined @@ -681,6 +690,32 @@ const addAvailable = (self: Pool, item: PoolItem): void => { self.state.availableTail = item } +/** + * Returns a released item at the front, so the next borrow gets the one used + * most recently. Borrowers take from the front, so the list runs warmest + * first. + * + * Sending it to the back instead spreads a sequence of borrows evenly over + * every item the pool has open. For a pool of connections that means none of + * them is ever the hot one - each borrow lands on a peer that has been sitting + * idle, losing whatever warmth it had - and it means `timeToLive` never + * reclaims anything, because a pool that grew for one burst keeps every item + * equally fresh forever. Under saturation the two orders agree, since every + * item is checked out either way. + */ +const addAvailableFront = (self: Pool, item: PoolItem): void => { + if (item.isAvailable || reservations.has(item)) return + item.isAvailable = true + item.availablePrevious = undefined + item.availableNext = self.state.availableHead + if (self.state.availableHead !== undefined) { + self.state.availableHead.availablePrevious = item + } else { + self.state.availableTail = item + } + self.state.availableHead = item +} + const removeAvailable = (self: Pool, item: PoolItem): void => { if (!item.isAvailable) return item.isAvailable = false @@ -718,9 +753,9 @@ const removeAvailable = (self: Pool, item: PoolItem): void => * @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 => + (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) { @@ -732,6 +767,60 @@ export const invalidate: { return Effect.void })) +/** + * Reserves a leased item for exclusive use until the scope closes. This + * removes the item's remaining capacity from the pool but does not wait + * for existing leases to finish. It has no effect when per-item concurrency is + * `1` or the pool does not contain the item. + * + * @see {@link get} for acquiring an item + * + * @category combinators + * @since 4.0.0 + */ +export const reserve: { + (item: A): (self: Pool) => Effect.Effect + (self: Pool, item: A): Effect.Effect +} = dual( + 2, + (self: Pool, item: A): Effect.Effect => + Effect.asVoid(Effect.acquireRelease( + Effect.sync(() => { + if (self.config.concurrency === 1) return undefined + for (const poolItem of self.state.items) { + if (poolItem.exit._tag !== "Success" || poolItem.exit.value !== item) continue + const existing = reservations.get(poolItem) + if (existing === undefined) self.state.usage += self.config.concurrency - 1 + reservations.set(poolItem, (existing ?? 0) + 1) + removeAvailable(self, poolItem) + return poolItem + } + return undefined + }), + (poolItem) => + core.withFiber((fiber) => { + if (poolItem === undefined) return internal.void + const remaining = (reservations.get(poolItem) ?? 1) - 1 + if (remaining > 0) { + reservations.set(poolItem, remaining) + return internal.void + } + reservations.delete(poolItem) + self.state.usage -= self.config.concurrency - 1 + if ( + !self.state.isShuttingDown && + self.state.items.has(poolItem) && + !self.state.invalidated.has(poolItem) && + poolItem.refCount < self.config.concurrency + ) { + addAvailableFront(self, poolItem) + wakeWaiters(self, fiber, self.config.concurrency - poolItem.refCount) + } + return internal.void + }) + )) +) + const invalidatePoolItem = (self: Pool, poolItem: PoolItem): Effect.Effect => Effect.suspend(() => { if (!self.state.items.has(poolItem)) { @@ -747,7 +836,13 @@ const invalidatePoolItem = (self: Pool, poolItem: PoolItem): E } self.state.invalidated.add(poolItem) removeAvailable(self, poolItem) - return Effect.void + // An invalidated item stops counting towards the pool's active size, so the + // pool is now below target and has to top itself back up. Waiting for the + // last lease to be returned would strand anybody already queued: the item + // they are waiting for is never coming back. + return Effect.asVoid( + Effect.forkIn(Effect.interruptible(resize(self)), self.state.scope, { startImmediately: true }) + ) }) const resize = (self: Pool): Effect.Effect => @@ -906,7 +1001,7 @@ const strategyUsageTTL = Effect.fnUntraced(function*(ttl: Duration.Input) return Effect.undefined } const item = Iterable.head( - Iterable.filter(pool.state.invalidated, (item) => !item.disableReclaim) + Iterable.filter(pool.state.invalidated, (item) => !item.disableReclaim && !reservations.has(item)) ) if (item._tag === "None") { return Effect.undefined diff --git a/repos/effect/packages/effect/src/PrimaryKey.ts b/repos/effect/packages/effect/src/PrimaryKey.ts index ed76a3bc28..f7ddfa6563 100644 --- a/repos/effect/packages/effect/src/PrimaryKey.ts +++ b/repos/effect/packages/effect/src/PrimaryKey.ts @@ -24,7 +24,7 @@ import { hasProperty } from "./Predicate.ts" * @category symbols * @since 2.0.0 */ -export const symbol = "~effect/interfaces/PrimaryKey" +export const symbol = "~effect/PrimaryKey" /** * An interface for objects that can provide a string-based primary key. diff --git a/repos/effect/packages/effect/src/PubSub.ts b/repos/effect/packages/effect/src/PubSub.ts index 3127101cba..6861798fd6 100644 --- a/repos/effect/packages/effect/src/PubSub.ts +++ b/repos/effect/packages/effect/src/PubSub.ts @@ -17,6 +17,7 @@ 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 Count from "./internal/count.ts" import * as Latch from "./Latch.ts" import * as MutableList from "./MutableList.ts" import * as MutableRef from "./MutableRef.ts" @@ -1239,7 +1240,9 @@ const pollForItem = (self: Subscription) => { } /** - * Takes up to the specified number of messages from the subscription without suspending. + * Takes up to the specified number of messages from the subscription without + * suspending. Finite fractional values are rounded down, while `NaN` and + * non-positive values are treated as `0`. * * **Example** (Taking up to a maximum number of messages) * @@ -1278,6 +1281,7 @@ export const takeUpTo: { } = dual(2, (self: Subscription, max: number): Effect.Effect> => Effect.suspend(() => { if (self.shutdownFlag.current) return Effect.interrupt + max = Count.normalize(max) let replay: Array | undefined = undefined if (self.replayWindow.remaining >= max) { return Effect.succeed(self.replayWindow.takeN(max)) @@ -1293,8 +1297,10 @@ export const takeUpTo: { })) /** - * Takes between the specified minimum and maximum number of messages from the subscription. - * Will suspend if the minimum number is not immediately available. + * Takes between the specified minimum and maximum number of messages from the + * subscription. Finite fractional bounds are rounded down, while `NaN` and + * non-positive bounds are treated as `0`. Will suspend if the normalized + * minimum number is not immediately available. * * **Example** (Taking between a minimum and maximum) * @@ -1329,7 +1335,7 @@ export const takeBetween: { } = dual( 3, (self: Subscription, min: number, max: number): Effect.Effect> => - Effect.suspend(() => takeRemainderLoop(self, min, max, [])) + Effect.suspend(() => takeRemainderLoop(self, Count.normalize(min), Count.normalize(max), [])) ) const takeRemainderLoop = ( @@ -1654,6 +1660,7 @@ class BoundedPubSubArbSubscription implements PubSub.BackingSubscripti } pollUpTo(n: number): Array { + n = Count.normalize(n) if (this.unsubscribed) { return [] } @@ -1850,6 +1857,7 @@ class BoundedPubSubPow2Subscription implements PubSub.BackingSubscript } pollUpTo(n: number): Array { + n = Count.normalize(n) if (this.unsubscribed) { return [] } @@ -2012,28 +2020,22 @@ class BoundedPubSubSingleSubscription implements PubSub.BackingSubscri if (this.self.subscribers === 0) { this.self.value = AbsentValue as unknown as A } - this.subscriberIndex += 1 + this.subscriberIndex = this.self.publisherIndex return elem } pollUpTo(n: number): Array { - if (this.isEmpty() || n < 1) { + if (Count.normalize(n) < 1 || this.isEmpty()) { return [] } - const a = this.self.value - this.self.subscribers -= 1 - if (this.self.subscribers === 0) { - this.self.value = AbsentValue as unknown as A - } - this.subscriberIndex += 1 - return [a] + return [this.poll() as A] } unsubscribe(): void { if (!this.unsubscribed) { this.unsubscribed = true this.self.subscriberCount -= 1 - if (this.subscriberIndex !== this.self.publisherIndex) { + if (this.self.subscribers !== 0 && this.subscriberIndex !== this.self.publisherIndex) { this.self.subscribers -= 1 if (this.self.subscribers === 0) { this.self.value = AbsentValue as unknown as A @@ -2210,6 +2212,7 @@ class UnboundedPubSubSubscription implements PubSub.BackingSubscriptio } pollUpTo(n: number): Array { + n = Count.normalize(n) const builder: Array = [] let i = 0 while (i !== n) { @@ -2791,6 +2794,7 @@ class ReplayWindowImpl implements PubSub.ReplayWindow { return value as A } takeN(n: number): Array { + n = Count.normalize(n) const len = Math.min(n, this.remaining) const items = new Array(len) for (let i = 0; i < len; i++) { diff --git a/repos/effect/packages/effect/src/Queue.ts b/repos/effect/packages/effect/src/Queue.ts index cad92efc3f..91a8004ee3 100644 --- a/repos/effect/packages/effect/src/Queue.ts +++ b/repos/effect/packages/effect/src/Queue.ts @@ -16,6 +16,7 @@ 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 Count from "./internal/count.ts" import * as internalEffect from "./internal/effect.ts" import * as MutableList from "./MutableList.ts" import * as Option from "./Option.ts" @@ -1294,9 +1295,10 @@ export const collect = (self: Dequeue): Effect, Pull * **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. + * queue's batching rules. Finite fractional values of `n` are rounded down. + * If `n` is `NaN` or non-positive, 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) * @@ -1337,9 +1339,10 @@ export const takeN = ( * **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. + * available. It returns at most `max` messages. Finite fractional bounds are + * rounded down, while `NaN` and non-positive bounds are treated as `0`. 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) * @@ -1373,10 +1376,13 @@ export const takeBetween = ( self: Dequeue, min: number, max: number -): Effect, E> => - internalEffect.suspend(() => +): Effect, E> => { + min = Count.normalize(min) + max = Count.normalize(max) + return 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 @@ -1558,16 +1564,89 @@ export const takeUnsafe = (self: Dequeue): Exit | undefined => releaseCapacity(self) return core.exitSucceed(message) } else if (self.capacity <= 0 && self.state.offers.size > 0) { - self.capacity = 1 - releaseCapacity(self) - self.capacity = 0 - const message = MutableList.take(self.messages)! + const message = takeOfferUnsafe(self.state.offers) releaseCapacity(self) return core.exitSucceed(message) } return undefined } +/** + * Manually releases current queue takers synchronously. + * + * **When to use** + * + * Use when synchronous offers should release waiting consumers immediately + * instead of waiting for the scheduled release task. + * + * **Details** + * + * This immediately runs the queue's taker-release pass instead of waiting for + * its scheduled task. It does not complete the queue or resume fibers waiting + * on `Queue.await`. + * + * **Example** (Releasing a waiting taker synchronously) + * + * ```ts import.meta.vitest + * import { Effect, Fiber, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.unbounded() + * const taker = yield* Queue.take(queue).pipe(Effect.forkChild) + * yield* Effect.yieldNow + * + * Queue.offerUnsafe(queue, 1) + * Queue.flushUnsafe(queue) + * + * return yield* Fiber.join(taker) + * }) + * + * await Effect.runPromise(program) // => 1 + * ``` + * + * @category offering + * @since 4.0.0 + */ +export const flushUnsafe = (self: Enqueue): void => releaseTakers(self) + +/** + * Manually releases current queue takers. + * + * **When to use** + * + * Use when synchronous offers should release waiting consumers through an + * `Effect` instead of waiting for the scheduled release task. + * + * **Details** + * + * This immediately runs the queue's taker-release pass instead of waiting for + * its scheduled task. It does not complete the queue or resume fibers waiting + * on `Queue.await`. + * + * **Example** (Releasing a waiting taker) + * + * ```ts import.meta.vitest + * import { Effect, Fiber, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.unbounded() + * const taker = yield* Queue.take(queue).pipe(Effect.forkChild) + * yield* Effect.yieldNow + * + * Queue.offerUnsafe(queue, 1) + * yield* Queue.flush(queue) + * + * return yield* Fiber.join(taker) + * }) + * + * await Effect.runPromise(program) // => 1 + * ``` + * + * @category offering + * @since 4.0.0 + */ +export const flush = (self: Enqueue): Effect => internalEffect.sync(() => flushUnsafe(self)) + const await_ = (self: Dequeue): Effect> => internalEffect.callback>((resume) => { const awaiter = (effect: Effect) => resume(Pull.catchDone(effect, () => internalEffect.exitVoid)) @@ -1836,7 +1915,6 @@ const exitFailDone = core.exitFail(core.Done()) as Failure const exitInterrupt = internalEffect.exitInterrupt() as Failure const releaseTakers = (self: Enqueue) => { - self.scheduleRunning = false if (self.state._tag === "Done" || self.state.takers.size === 0) { return } @@ -1854,7 +1932,10 @@ const scheduleReleaseTaker = (self: Enqueue) => { return } self.scheduleRunning = true - self.dispatcher.scheduleTask(() => releaseTakers(self), 0) + self.dispatcher.scheduleTask(() => { + self.scheduleRunning = false + releaseTakers(self) + }, 0) } const takeBetweenUnsafe = ( @@ -1866,11 +1947,8 @@ const takeBetweenUnsafe = ( return self.state.exit } else if (max <= 0 || min <= 0) { return core.exitSucceed([]) - } else if (self.capacity <= 0 && self.state.offers.size > 0) { - self.capacity = 1 - releaseCapacity(self) - self.capacity = 0 - const messages = [MutableList.take(self.messages)!] + } else if (self.capacity <= 0 && self.messages.length === 0 && self.state.offers.size > 0) { + const messages = [takeOfferUnsafe(self.state.offers)] releaseCapacity(self) return core.exitSucceed(messages) } @@ -1917,6 +1995,22 @@ const offerRemainingArray = (self: Enqueue, remaining: Array) => }) } +// Reserve a pending message for the consumer before the producer can reenter. +const takeOfferUnsafe = (offers: Set>): A => { + const entry = offers.values().next().value! + if (entry._tag === "Single") { + offers.delete(entry) + entry.resume(exitTrue) + return entry.message + } + const message = entry.remaining[entry.offset++] + if (entry.offset === entry.remaining.length) { + offers.delete(entry) + entry.resume(core.exitSucceed([])) + } + return message +} + const releaseCapacity = (self: Dequeue): boolean => { if (self.state._tag === "Done") { return Pull.isDoneCause(self.state.exit.cause) @@ -1930,22 +2024,22 @@ const releaseCapacity = (self: Dequeue): boolean => { } return false } - let n = self.capacity - self.messages.length + // Resuming a producer can synchronously take, offer, or shut down this queue. for (const entry of self.state.offers) { - if (n === 0) break + let n = self.capacity - self.messages.length + if (n <= 0) break else if (entry._tag === "Single") { MutableList.append(self.messages, entry.message) - n-- - entry.resume(exitTrue) self.state.offers.delete(entry) + entry.resume(exitTrue) } else { for (; entry.offset < entry.remaining.length; entry.offset++) { if (n === 0) return false MutableList.append(self.messages, entry.remaining[entry.offset]) n-- } - entry.resume(core.exitSucceed([])) self.state.offers.delete(entry) + entry.resume(core.exitSucceed([])) } } return false @@ -1970,10 +2064,7 @@ const takeAllUnsafe = (self: Dequeue) => { releaseCapacity(self) return messages } else if (self.state._tag !== "Done" && self.state.offers.size > 0) { - self.capacity = 1 - releaseCapacity(self) - self.capacity = 0 - const messages = [MutableList.take(self.messages)!] + const messages = [takeOfferUnsafe(self.state.offers)] releaseCapacity(self) return messages } diff --git a/repos/effect/packages/effect/src/Random.ts b/repos/effect/packages/effect/src/Random.ts index 2afe37ea19..67b2c57814 100644 --- a/repos/effect/packages/effect/src/Random.ts +++ b/repos/effect/packages/effect/src/Random.ts @@ -17,6 +17,23 @@ import * as random from "./internal/random.ts" import type * as NonEmptyIterable from "./NonEmptyIterable.ts" import * as Predicate from "./Predicate.ts" +/** + * The service used to generate pseudo-random numbers. + * + * @category services + * @since 4.0.0 + */ +export interface Random { + /** + * Generates a random safe integer. + */ + nextIntUnsafe(): number + /** + * Generates a random number between 0 (inclusive) and 1 (exclusive). + */ + nextDoubleUnsafe(): number +} + /** * Represents a service for generating pseudo-random numbers. * @@ -49,12 +66,9 @@ import * as Predicate from "./Predicate.ts" * @category services * @since 2.0.0 */ -export const Random: Context.Reference<{ - nextIntUnsafe(): number - nextDoubleUnsafe(): number -}> = random.Random +export const Random: Context.Reference = random.Random -const randomWith = (f: (random: typeof Random["Service"]) => A): Effect.Effect => +const randomWith = (f: (random: Random) => A): Effect.Effect => Effect.withFiber((fiber) => Effect.succeed(f(fiber.getRef(Random)))) /** @@ -139,7 +153,7 @@ export const nextInt: Effect.Effect = randomWith((r) => r.nextIntUnsafe( * @since 4.0.0 */ export const nextBetween = (min: number, max: number): Effect.Effect => - randomWith((r) => r.nextDoubleUnsafe() * (max - min) + min) + randomWith((r) => random.nextBetween(min, max, r.nextDoubleUnsafe())) /** * Generates a random integer between `min` and `max`. diff --git a/repos/effect/packages/effect/src/RcMap.ts b/repos/effect/packages/effect/src/RcMap.ts index 1e00423de8..e484c4fd23 100644 --- a/repos/effect/packages/effect/src/RcMap.ts +++ b/repos/effect/packages/effect/src/RcMap.ts @@ -274,7 +274,7 @@ export const make: { self.state = { _tag: "Closed" } return Effect.forEach( map, - ([, entry]) => Effect.exit(Scope.close(entry.scope, Exit.void)) + ([, entry]) => Effect.exit(closeEntry(entry)) ).pipe( Effect.tap(() => Effect.sync(() => { @@ -370,7 +370,7 @@ export const get: { context.set(key, value) }) context.set(Scope.Scope.key, entry.scope) - self.lookup(key).pipe( + Effect.suspend(() => self.lookup(key)).pipe( Effect.runForkWith(Context.makeUnsafe(context)), Fiber.runIn(entry.scope) ).addObserver((exit) => Deferred.doneUnsafe(entry.deferred, exit)) @@ -449,20 +449,26 @@ export const getOption: { }) ) +const closeEntry = (entry: State.Entry) => + entry.fiber + ? Fiber.interrupt(entry.fiber).pipe(Effect.andThen(Scope.close(entry.scope, Exit.void))) + : Scope.close(entry.scope, Exit.void) + 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 (self.state._tag === "Closed") { + return closeEntry(entry) + } + + const o = MutableHashMap.get(self.state.map, key) + if (o._tag === "None" || o.value !== entry) { + return closeEntry(entry) + } else if (Duration.isZero(entry.idleTimeToLive)) { + MutableHashMap.remove(self.state.map, key) + return closeEntry(entry) } else if (!Duration.isFinite(entry.idleTimeToLive)) { return Effect.void } @@ -476,6 +482,8 @@ const release = (self: RcMap, key: K, entry: State.Entry const remaining = entry.expiresAt - now if (remaining <= 0) { if (self.state._tag === "Closed" || entry.refCount > 0) return Effect.void + const o = MutableHashMap.get(self.state.map, key) + if (o._tag === "None" || o.value !== entry) return Effect.void MutableHashMap.remove(self.state.map, key) return restore(Scope.close(entry.scope, Exit.void)) } @@ -592,8 +600,7 @@ export const invalidate: { 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) + yield* closeEntry(entry) }, Effect.uninterruptible) ) diff --git a/repos/effect/packages/effect/src/Redacted.ts b/repos/effect/packages/effect/src/Redacted.ts index 881ec810d6..be67a9e65e 100644 --- a/repos/effect/packages/effect/src/Redacted.ts +++ b/repos/effect/packages/effect/src/Redacted.ts @@ -19,7 +19,7 @@ import type { Pipeable } from "./Pipeable.ts" import { hasProperty, isString } from "./Predicate.ts" import type { Covariant } from "./Types.ts" -const TypeId = "~effect/data/Redacted" +const TypeId = "~effect/Redacted" /** * A wrapper for sensitive values whose string, JSON, and inspection output is diff --git a/repos/effect/packages/effect/src/RequestResolver.ts b/repos/effect/packages/effect/src/RequestResolver.ts index 2061d4b860..951c5f7151 100644 --- a/repos/effect/packages/effect/src/RequestResolver.ts +++ b/repos/effect/packages/effect/src/RequestResolver.ts @@ -17,7 +17,8 @@ import type * as Duration from "./Duration.ts" import * as Effect from "./Effect.ts" import * as Exit from "./Exit.ts" import { constTrue, dual, identity } from "./Function.ts" -import { exitFail, exitSucceed } from "./internal/core.ts" +import { exitSucceed } from "./internal/core.ts" +import * as Count from "./internal/count.ts" import * as effect from "./internal/effect.ts" import * as internal from "./internal/request.ts" import * as Iterable from "./Iterable.ts" @@ -528,17 +529,18 @@ export const fromEffectTagged = - Effect.matchCause((fns[tag] as any)(requests) as Effect.Effect, unknown, unknown>, { + Effect.matchCause((fns[tag] as any)(requests) as Effect.Effect, unknown, unknown>, { onFailure: (cause) => { for (let i = 0; i < requests.length; i++) { const entry = requests[i] - entry.completeUnsafe(exitFail(cause) as any) + entry.completeUnsafe(Exit.failCause(cause) as any) } }, onSuccess: (res) => { - for (let i = 0; i < res.length; i++) { - const entry = requests[i] - entry.completeUnsafe(exitSucceed(res[i]) as any) + let i = 0 + for (const result of res) { + const entry = requests[i++] + entry.completeUnsafe(exitSucceed(result) as any) } } }), @@ -740,7 +742,8 @@ export const never: RequestResolver = make(() => Effect.never) * * 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. + * batches. Finite fractional values of `n` are rounded down. `NaN` and + * non-positive values are treated as `1` so that every batch remains non-empty. * * **Example** (Limiting parallel request batches) * @@ -787,11 +790,13 @@ export const never: RequestResolver = make(() => Effect.never) export const batchN: { (n: number): (self: RequestResolver) => RequestResolver (self: RequestResolver, n: number): RequestResolver -} = dual(2, (self: RequestResolver, n: number): RequestResolver => - makeWith({ +} = dual(2, (self: RequestResolver, n: number): RequestResolver => { + const size = Count.normalizeNonEmpty(n) + return makeWith({ ...self, - collectWhile: (requests) => requests.size < n - })) + collectWhile: (requests) => requests.size < size + }) +}) /** * Transforms a request resolver by grouping requests using the specified key @@ -1114,8 +1119,9 @@ export const asCache: { * * **Gotchas** * - * Entries do not expire by time, and completed failures are cached the same as - * successes. Request equality controls cache hits. + * Entries do not expire by time, and completed failures without interruptions + * are cached the same as successes. Results containing interruptions are not + * cached. Request equality controls cache hits. * * @see {@link asCache} for exposing the resolver as a `Cache` with time-to-live and service lookup controls * @see {@link persisted} for backing persistable requests with the configured persistence store @@ -1163,7 +1169,16 @@ export const withCache: { MutableHashMap.set(cache, entry.request, cached) const prevComplete = entry.completeUnsafe entry.completeUnsafe = function(exit) { - cached.exit = exit as any + if (Exit.hasInterrupts(exit)) { + if (cached.exit === undefined) { + const current = MutableHashMap.get(cache, entry.request) + if (current._tag === "Some" && current.value === cached) { + MutableHashMap.remove(cache, entry.request) + } + } + } else { + cached.exit = exit as any + } prevComplete(exit) } return true @@ -1261,6 +1276,7 @@ export const persisted: { >) const leftover: Array> = [] const toPersist = new Map>() + const completed = new Set>() for (let i = 0; i < results.length; i++) { const entry = entries[i] const exit = results[i] @@ -1270,6 +1286,7 @@ export const persisted: { ) { const prevComplete = entry.completeUnsafe entry.completeUnsafe = function(exit) { + completed.add(entry) toPersist.set(entry.request, exit as any) prevComplete(exit) } @@ -1281,10 +1298,10 @@ export const persisted: { if (!Arr.isArrayNonEmpty(leftover)) { return } - yield* Effect.catchCause(self.runAll(leftover, key), (cause) => { + yield* Effect.catchCause(Effect.suspend(() => self.runAll(leftover, key)), (cause) => { for (let i = 0; i < leftover.length; i++) { const entry = leftover[i] - if (!toPersist.has(entry.request)) continue + if (completed.has(entry)) continue entry.completeUnsafe(Exit.failCause(cause) as any) } return Effect.void diff --git a/repos/effect/packages/effect/src/Result.ts b/repos/effect/packages/effect/src/Result.ts index eb7a4e014c..262a9493a6 100644 --- a/repos/effect/packages/effect/src/Result.ts +++ b/repos/effect/packages/effect/src/Result.ts @@ -27,7 +27,7 @@ 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" +const TypeId = "~effect/Result" /** * A value that is either `Success` or `Failure`. diff --git a/repos/effect/packages/effect/src/Scheduler.ts b/repos/effect/packages/effect/src/Scheduler.ts index 1a6b7f87d6..a60495eb06 100644 --- a/repos/effect/packages/effect/src/Scheduler.ts +++ b/repos/effect/packages/effect/src/Scheduler.ts @@ -80,25 +80,35 @@ export const Scheduler: Context.Reference = Context.Reference new MixedScheduler() }) -const setImmediate = "setImmediate" in globalThis - ? (f: () => void) => { +const setMicrotask = (f: () => void) => { + let cancelled = false + Promise.resolve().then(() => { + if (!cancelled) f() + }) + return (): void => { + cancelled = true + } +} + +const setTimer: (f: () => void) => () => void = "setImmediate" in globalThis + ? (f) => { // @ts-ignore const timer = globalThis.setImmediate(f) // @ts-ignore return (): void => globalThis.clearImmediate(timer) } - : (f: () => void) => { + : (f) => { const timer = setTimeout(f, 0) return (): void => clearTimeout(timer) } -const setMicrotask = (f: () => void) => { - let cancelled = false - Promise.resolve().then(() => { - if (!cancelled) f() - }) - return (): void => { - cancelled = true +// Some runtimes (e.g. Cloudflare Workers) throw when a timer is set in global +// scope. Fall back to a microtask so effects can still yield at module load. +const setImmediate = (f: () => void) => { + try { + return setTimer(f) + } catch { + return setMicrotask(f) } } @@ -172,7 +182,7 @@ export class MixedScheduler implements Scheduler { * @since 2.0.0 */ shouldYield(fiber: Fiber.Fiber) { - return fiber.currentOpCount >= fiber.maxOpsBeforeYield + return fiber.currentOpCount >= fiber.cache.maxOpsBeforeYield } /** diff --git a/repos/effect/packages/effect/src/Schema.ts b/repos/effect/packages/effect/src/Schema.ts index 2924d92fcd..ab871a45e2 100644 --- a/repos/effect/packages/effect/src/Schema.ts +++ b/repos/effect/packages/effect/src/Schema.ts @@ -12,10 +12,10 @@ * @since 4.0.0 */ -/** @effect-diagnostics schemaStructWithTag:skip-file */ import * as Arr from "./Array.ts" import * as BigDecimal_ from "./BigDecimal.ts" import type * as Brand from "./Brand.ts" +import * as ByteSize_ from "./ByteSize.ts" import * as Cause_ from "./Cause.ts" import * as Chunk_ from "./Chunk.ts" import * as Data from "./Data.ts" @@ -25,12 +25,12 @@ 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 type * as Equivalence from "./Equivalence.ts" import * as Exit_ from "./Exit.ts" import type { Formatter } from "./Formatter.ts" import { format, formatPropertyKey } from "./Formatter.ts" -import { identity, memoize } from "./Function.ts" -import * as Graph_ from "./Graph.ts" +import { identity } from "./Function.ts" +import type * as Graph_ from "./Graph.ts" import * as HashMap_ from "./HashMap.ts" import * as HashSet_ from "./HashSet.ts" import * as core from "./internal/core.ts" @@ -38,16 +38,22 @@ import { effectIsExit } from "./internal/effect.ts" import * as InternalGraph from "./internal/graph.ts" import * as InternalRecord from "./internal/record.ts" import * as InternalAnnotations from "./internal/schema/annotations.ts" -import * as InternalSchema from "./internal/schema/schema.ts" -import * as InternalArbitrary from "./internal/schema/toArbitrary.ts" +import * as InternalMake from "./internal/schema/make.ts" +import * as InternalStandardSchema from "./internal/schema/standardSchema.ts" +import * as InternalToCodec from "./internal/schema/toCodec.ts" +import * as InternalToDifferJsonPatch from "./internal/schema/toDifferJsonPatch.ts" +import * as InternalToEncoderXml from "./internal/schema/toEncoderXml.ts" import * as InternalEquivalence from "./internal/schema/toEquivalence.ts" +import * as InternalToFormatter from "./internal/schema/toFormatter.ts" +import * as InternalToIso from "./internal/schema/toIso.ts" import * as InternalToJsonSchemaDocument from "./internal/schema/toJsonSchemaDocument.ts" import * as InternalToRepresentation from "./internal/schema/toRepresentation.ts" +import { isSchemaError as isSchemaErrorInternal, SchemaErrorTypeId } from "./internal/schemaError.ts" import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts" -import * as JsonPatch from "./JsonPatch.ts" -import * as JsonSchema from "./JsonSchema.ts" +import type * as JsonPatch from "./JsonPatch.ts" +import type * as JsonSchema from "./JsonSchema.ts" import { remainder } from "./Number.ts" -import * as Optic_ from "./Optic.ts" +import type * as Optic_ from "./Optic.ts" import * as Option_ from "./Option.ts" import * as Order from "./Order.ts" import * as Pipeable from "./Pipeable.ts" @@ -56,7 +62,6 @@ import * as Record_ from "./Record.ts" import * as Redacted_ from "./Redacted.ts" import * as RegExp_ from "./RegExp.ts" import * as Result_ from "./Result.ts" -import * as Scheduler from "./Scheduler.ts" import * as SchemaAST from "./SchemaAST.ts" import * as SchemaGetter from "./SchemaGetter.ts" import * as SchemaIssue from "./SchemaIssue.ts" @@ -66,12 +71,16 @@ import * as SchemaTransformation from "./SchemaTransformation.ts" import type { StandardJSONSchemaV1, StandardSchemaV1 } from "./StandardSchema.ts" import type { Assign, Lambda, Mutable, Simplify } from "./Struct.ts" import * as Struct_ from "./Struct.ts" -import type * as FastCheck from "./testing/FastCheck.ts" import type { RequiredKeys, UnionToIntersection } from "./Types.ts" import type { Unify } from "./Unify.ts" +import * as Cookies_ from "./unstable/http/Cookies.ts" +import * as Headers_ from "./unstable/http/Headers.ts" +import * as UrlParams_ from "./unstable/http/UrlParams.ts" +import * as IpInterface_ from "./unstable/net/IpInterface.ts" +import * as IpNetwork_ from "./unstable/net/IpNetwork.ts" +import * as NetAddress_ from "./unstable/net/NetAddress.ts" -const TypeId = InternalSchema.TypeId - +const TypeId = InternalMake.TypeId /** * Whether a schema field is required or optional within a struct. * @@ -82,7 +91,6 @@ const TypeId = InternalSchema.TypeId * @since 4.0.0 */ export type Optionality = "required" | "optional" - /** * Whether a schema field is readonly or mutable within a struct. * @@ -92,7 +100,6 @@ export type Optionality = "required" | "optional" * @since 4.0.0 */ export type Mutability = "readonly" | "mutable" - /** * Whether a schema field has a constructor default value. * @@ -103,7 +110,6 @@ export type Mutability = "readonly" | "mutable" * @since 4.0.0 */ export type ConstructorDefault = "no-default" | "with-default" - /** * Options for `makeEffect`, `make`, and Class constructors. * @@ -134,7 +140,6 @@ export interface MakeOptions { readonly value: unknown } } - /** * The fully-parameterized schema interface without a construct signature. * Exposes all 14 type parameters controlling type inference, mutability, @@ -166,26 +171,21 @@ export interface BottomWithoutNew< out EncodedOptionality extends Optionality = "required" > extends Pipeable.Pipeable { readonly [TypeId]: typeof TypeId - readonly "ast": Ast readonly "Rebuild": Rebuild readonly "~type.parameters": TypeParameters - readonly "Type": T readonly "Encoded": E readonly "DecodingServices": RD readonly "EncodingServices": RE - readonly "~type.make.in": TypeMakeIn - readonly "~type.make": TypeMake // useful to type the `refine` interface + readonly "~type.make": TypeMake readonly "~type.constructor.default": TypeConstructorDefault readonly "Iso": Iso - readonly "~type.mutability": TypeMutability readonly "~type.optionality": TypeOptionality readonly "~encoded.mutability": EncodedMutability readonly "~encoded.optionality": EncodedOptionality - annotate(annotations: Annotations.Bottom): this["Rebuild"] annotateKey(annotations: Annotations.Key): this["Rebuild"] check(...checks: readonly [SchemaAST.Check, ...Array>]): this["Rebuild"] @@ -259,7 +259,6 @@ export interface BottomWithoutNew< */ makeEffect(input: this["~type.make.in"], options?: MakeOptions): Effect.Effect } - /** * Fully-parameterized base interface for schemas that can be extended directly * by TypeScript classes. @@ -317,7 +316,6 @@ export interface Bottom< { new(_: never): {} } - /** * Lazy `BottomWithoutNew` variant for schema implementations that * compute their public views on demand. @@ -370,7 +368,6 @@ export interface BottomLazyWithoutNew< EncodedOptionality > {} - /** * Lazy `Bottom` variant for schemas that can be extended directly by TypeScript * classes. @@ -414,7 +411,6 @@ export interface BottomLazy< { new(_: never): {} } - /** * Type-level representation returned by {@link declareConstructor}. * @@ -434,7 +430,6 @@ export interface declareConstructor {} - /** * Creates a schema for a **parametric** type (a generic container such as * `Array`, `Option`, etc.) by accepting a list of type-parameter schemas @@ -513,7 +508,6 @@ export function declareConstructor() { ) } } - /** * Type-level representation returned by {@link declare}. * @@ -523,7 +517,6 @@ export function declareConstructor() { export interface declare extends declareConstructor { readonly "Rebuild": declare } - /** * Creates a schema for a **non-parametric** opaque type using a type-guard * function. The schema accepts any unknown value and succeeds when `is` returns @@ -569,7 +562,6 @@ export function declare( annotations ) } - /** * Returns a schema widened to the fully-parameterized {@link Bottom} interface, * making all 14 type parameters visible to TypeScript. @@ -620,7 +612,6 @@ export function revealBottom( > { return bottom } - /** * Adds metadata annotations to a schema without changing its runtime behavior. * This is the pipeable (curried) counterpart of the `.annotate` method. @@ -653,7 +644,6 @@ export function revealBottom( export function annotate(annotations: Annotations.Bottom) { return (self: S) => self.annotate(annotations) } - /** * Adds metadata annotations to the **encoded** side of a schema without * changing its runtime behavior. This is the encoded-side counterpart of @@ -686,7 +676,6 @@ export function annotate(annotations: Annotations.Bottom(annotations: Annotations.Bottom) { return (self: S): S["Rebuild"] => flip(flip(self).annotate(annotations)) } - /** * Adds key-level annotations to a schema field. This is the pipeable * (curried) counterpart of the `.annotateKey` method. @@ -723,7 +712,6 @@ export function annotateKey(annotations: Annotations.Key {} - /** * Lightweight structural constraint for APIs that accept schema values but only * read their data and type-level views. @@ -804,7 +791,6 @@ export interface Constraint { readonly "~encoded.optionality": Optionality readonly "~encoded.mutability": Mutability } - /** * Lightweight structural constraint for APIs that need codec type views but do * not need the full schema protocol. @@ -827,7 +813,6 @@ export interface ConstraintCodec extends ConstraintCodec {} - /** * Lightweight structural constraint for APIs that need encoder type views but * do not need the full schema protocol. @@ -865,7 +849,6 @@ export interface ConstraintDecoder extends ConstraintCode * @since 4.0.0 */ export interface ConstraintEncoder extends ConstraintCodec {} - /** * Lightweight structural constraint for APIs that need schema views and the * rebuilt schema type, but do not call the full schema protocol. @@ -882,7 +865,6 @@ export interface ConstraintEncoder extends ConstraintCode export interface ConstraintRebuildable extends Constraint { readonly "Rebuild": Constraint } - /** * Namespace of type-level helpers for {@link Schema}. * @@ -905,9 +887,11 @@ export declare namespace Schema { * @category utility types * @since 3.10.0 */ - export type Type = S extends { readonly "Type": infer T } ? T : never + type Type = S extends { + readonly "Type": infer T + } ? T : + never } - /** * A typed view of a schema that tracks only the decoded (output) type `T`. * @@ -942,7 +926,6 @@ export interface Schema extends Top { readonly "Type": T readonly "Rebuild": Schema } - /** * Namespace of type-level helpers for {@link Codec}. * @@ -965,8 +948,10 @@ export declare namespace Codec { * @category utility types * @since 3.10.0 */ - export type Encoded = S extends { readonly "Encoded": infer E } ? E : never - + type Encoded = S extends { + readonly "Encoded": infer E + } ? E : + never /** * Extracts the Effect services required during *decoding* from a schema. * @@ -983,8 +968,10 @@ export declare namespace Codec { * @category utility types * @since 4.0.0 */ - export type DecodingServices = S extends { readonly "DecodingServices": infer R } ? R : never - + type DecodingServices = S extends { + readonly "DecodingServices": infer R + } ? R : + never /** * Extracts the Effect services required during *encoding* from a schema. * @@ -1001,9 +988,11 @@ export declare namespace Codec { * @category utility types * @since 4.0.0 */ - export type EncodingServices = S extends { readonly "EncodingServices": infer R } ? R : never + type EncodingServices = S extends { + readonly "EncodingServices": infer R + } ? R : + never } - /** * A schema that tracks the decoded type `T`, the encoded type `E`, and the * Effect services required during decoding (`RD`) and encoding (`RE`). @@ -1044,7 +1033,6 @@ export interface Codec extends readonly "EncodingServices": RE readonly "Rebuild": Codec } - /** * A schema that tracks the decoded type `T` and the Effect services required * during decoding (`RD`). @@ -1067,7 +1055,6 @@ export interface Decoder extends Schema { readonly "EncodingServices": unknown readonly "Rebuild": Decoder } - /** * A schema that tracks the encoded type `E` and the Effect services required * during encoding (`RE`). @@ -1090,7 +1077,6 @@ export interface Encoder extends Schema { readonly "EncodingServices": RE readonly "Rebuild": Encoder } - /** * Returns a codec widened to the full {@link Codec} interface, prompting * TypeScript to infer all four type parameters (`T`, `E`, `RD`, `RE`). @@ -1119,7 +1105,6 @@ export interface Encoder extends Schema { export function revealCodec(codec: Codec) { return codec } - /** * A schema that additionally supports optic (lens/prism) operations. * @@ -1144,9 +1129,6 @@ export interface Optic extends Schema { readonly "EncodingServices": never readonly "Rebuild": Optic } - -const SchemaErrorTypeId = "~effect/SchemaError/SchemaError" - /** * Error thrown or returned when schema decoding or encoding fails. * @@ -1197,7 +1179,6 @@ export class SchemaError extends Data.TaggedError("SchemaError")<{ return `SchemaError(${this.message})` } } - /** * Returns `true` if `u` is a {@link SchemaError}. * @@ -1219,15 +1200,64 @@ export class SchemaError extends Data.TaggedError("SchemaError")<{ * @since 4.0.0 */ export function isSchemaError(u: unknown): u is SchemaError { - return Predicate.hasProperty(u, SchemaErrorTypeId) && u[SchemaErrorTypeId] === SchemaErrorTypeId + return isSchemaErrorInternal(u) +} + +function fromIssueEffect( + self: Effect.Effect +): Effect.Effect { + if (effectIsExit(self)) { + return fromIssueExit(self as Exit_.Exit) + } + return Effect.catchCause( + self, + (cause) => Effect.failCauseSync(() => Cause_.map(cause, (issue) => new SchemaError(issue))) + ) +} + +function fromIssueExit(exit: Exit_.Exit): Exit_.Exit { + return Exit_.isSuccess(exit) + ? exit as unknown as Exit_.Exit + : Exit_.failCause(Cause_.map(exit.cause, (issue) => new SchemaError(issue))) } -function makeStandardResult(exit: Exit_.Exit>): StandardSchemaV1.Result { - return Exit_.isSuccess(exit) ? exit.value : { - issues: [{ message: Cause_.pretty(exit.cause) }] +function getSchemaErrorOrThrow( + cause: Cause_.Cause, + message: string +): SchemaError { + let schemaError: SchemaError | undefined + for (const reason of cause.reasons) { + if (!Cause_.isFailReason(reason) || !isSchemaError(reason.error)) { + throw new globalThis.Error(message, { cause }) + } + schemaError ??= reason.error } + if (schemaError === undefined) { + throw new globalThis.Error(message, { cause }) + } + return schemaError +} + +function runSchemaErrorPromise( + self: Effect.Effect +): Promise { + return Effect.runPromiseExit(self).then((exit) => { + if (Exit_.isSuccess(exit)) { + return exit.value + } + throw getSchemaErrorOrThrow(exit.cause, "Promise adapter can only reject schema errors") + }) } +function runSchemaErrorSync( + self: Effect.Effect +): A { + const exit = Effect.runSyncExit(self) + if (Exit_.isSuccess(exit)) { + return exit.value + } + throw getSchemaErrorOrThrow(exit.cause, "Sync adapter can only throw schema errors") +} /** * Returns a "Standard Schema" object conforming to the [Standard Schema * v1](https://standardschema.dev/) specification. @@ -1304,67 +1334,8 @@ export function toStandardSchemaV1>( readonly parseOptions?: SchemaAST.ParseOptions | undefined } ): StandardSchemaV1 & S { - const decodeUnknownEffect = SchemaParser.decodeUnknownEffect(self) as ( - input: unknown, - options?: SchemaAST.ParseOptions - ) => Effect.Effect - const parseOptions: SchemaAST.ParseOptions = { errors: "all", ...options?.parseOptions } - const formatter = SchemaIssue.makeFormatterStandardSchemaV1(options) - const validate: StandardSchemaV1["~standard"]["validate"] = (value: unknown) => { - const scheduler = new Scheduler.MixedScheduler("sync") - const fiber = Effect.runFork( - Effect.match(decodeUnknownEffect(value, parseOptions), { - onFailure: formatter, - onSuccess: (value): StandardSchemaV1.Result => ({ value }) - }), - { scheduler } - ) - fiber.currentDispatcher?.flush() - const exit = fiber.pollUnsafe() - if (exit) { - return makeStandardResult(exit) - } - return new Promise((resolve) => { - fiber.addObserver((exit) => { - resolve(makeStandardResult(exit)) - }) - }) - } - if ("~standard" in self) { - const out = self as any - if ("validate" in out["~standard"]) return out - Object.assign(out["~standard"], { validate }) - return out - } else { - return Object.assign(self, { - "~standard": { - version: 1, - vendor: "effect", - validate - } as const - }) - } -} - -function toBaseStandardJSONSchemaV1(self: Constraint, target: StandardJSONSchemaV1.Target): JsonSchema.JsonSchema { - const doc2020_12 = toJsonSchemaDocument(self) - if (target === "draft-2020-12") { - const schema = doc2020_12.schema - if (Object.keys(doc2020_12.definitions).length > 0) { - schema.$defs = doc2020_12.definitions - } - return schema - } else if (target === "draft-07") { - const doc07 = JsonSchema.toDocumentDraft07(doc2020_12) - const schema = doc07.schema - if (Object.keys(doc07.definitions).length > 0) { - schema.definitions = doc07.definitions - } - return schema - } - throw new globalThis.Error(`Unsupported target: ${target}`) + return InternalStandardSchema.toStandardSchemaV1(self, options) } - /** * Converts a schema to an experimental Standard JSON Schema V1 representation. * @@ -1378,30 +1349,8 @@ function toBaseStandardJSONSchemaV1(self: Constraint, target: StandardJSONSchema export function toStandardJSONSchemaV1( self: S ): StandardJSONSchemaV1 & S { - const jsonSchema: StandardJSONSchemaV1.Props["jsonSchema"] = { - input(options) { - return toBaseStandardJSONSchemaV1(self, options.target) - }, - output(options) { - return toBaseStandardJSONSchemaV1(toType(self), options.target) - } - } - if ("~standard" in self) { - const out = self as any - if ("jsonSchema" in out["~standard"]) return out - Object.assign(out["~standard"], { jsonSchema }) - return out - } else { - return Object.assign(self, { - "~standard": { - version: 1, - vendor: "effect", - jsonSchema - } as const - }) - } + return InternalStandardSchema.toStandardJSONSchemaV1(self) } - /** * Creates a type guard function that checks if a value conforms to a given * schema. @@ -1439,8 +1388,7 @@ export function toStandardJSONSchemaV1( * @category guards * @since 3.10.0 */ -export const is = SchemaParser.is - +export const is: typeof SchemaParser.is = SchemaParser.is /** * Creates an assertion function that throws an error if the input does not match * the schema. @@ -1491,7 +1439,6 @@ export const is = SchemaParser.is */ export const asserts: (schema: S, input: I) => asserts input is I & S["Type"] = SchemaParser.asserts - /** * Decodes an `unknown` input against a schema, returning an `Effect` that * succeeds with the decoded value or fails with a {@link SchemaError}. @@ -1522,19 +1469,6 @@ export function decodeUnknownEffect(schema: S, options?: S return fromIssueEffect(parser(input, options)) } } - -function fromIssueEffect( - self: Effect.Effect -): Effect.Effect { - if (effectIsExit(self)) { - return fromIssueExit(self as Exit_.Exit) - } - return Effect.catchCause( - self, - (cause) => Effect.failCauseSync(() => Cause_.map(cause, (issue) => new SchemaError(issue))) - ) -} - /** * Decodes a typed input (the schema's `Encoded` type) against a schema, * returning an `Effect` that succeeds with the decoded value or fails with a @@ -1563,45 +1497,6 @@ export const decodeEffect: ( input: S["Encoded"], options?: SchemaAST.ParseOptions ) => Effect.Effect = decodeUnknownEffect - -function getSchemaErrorOrThrow( - cause: Cause_.Cause, - message: string -): SchemaError { - let schemaError: SchemaError | undefined - for (const reason of cause.reasons) { - if (!Cause_.isFailReason(reason) || !isSchemaError(reason.error)) { - throw new globalThis.Error(message, { cause }) - } - schemaError ??= reason.error - } - if (schemaError === undefined) { - throw new globalThis.Error(message, { cause }) - } - return schemaError -} - -function runSchemaErrorPromise( - self: Effect.Effect -): Promise { - return Effect.runPromiseExit(self).then((exit) => { - if (Exit_.isSuccess(exit)) { - return exit.value - } - throw getSchemaErrorOrThrow(exit.cause, "Promise adapter can only reject schema errors") - }) -} - -function runSchemaErrorSync( - self: Effect.Effect -): A { - const exit = Effect.runSyncExit(self) - if (Exit_.isSuccess(exit)) { - return exit.value - } - throw getSchemaErrorOrThrow(exit.cause, "Sync adapter can only throw schema errors") -} - /** * Decodes an `unknown` input against a schema synchronously, returning an * `Exit` that is either a `Success` with the decoded value or a `Failure`. @@ -1638,13 +1533,6 @@ export function decodeUnknownExit>(schema: return fromIssueExit(parser(input, options)) } } - -function fromIssueExit(exit: Exit_.Exit): Exit_.Exit { - return Exit_.isSuccess(exit) - ? exit as unknown as Exit_.Exit - : Exit_.failCause(Cause_.map(exit.cause, (issue) => new SchemaError(issue))) -} - /** * Decodes a typed input (the schema's `Encoded` type) against a schema * synchronously, returning an `Exit` that is either a `Success` with the decoded @@ -1679,7 +1567,6 @@ export const decodeExit: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Exit_.Exit = decodeUnknownExit - /** * Decodes an `unknown` input against a schema, returning an `Option` that is * `Some` with the decoded value on success or `None` for schema mismatches. @@ -1710,7 +1597,6 @@ export const decodeUnknownOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: unknown, options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.decodeUnknownOption - /** * Decodes a typed input (the schema's `Encoded` type) against a schema, * returning an `Option` that is `Some` with the decoded value on success or @@ -1740,7 +1626,6 @@ export const decodeOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.decodeOption - /** * Decodes an `unknown` input against a schema, returning a `Result` that * succeeds with the decoded value or fails with a {@link SchemaError} for schema @@ -1776,7 +1661,6 @@ export function decodeUnknownResult>(schema return Result_.mapError(parser(input, options), (issue) => new SchemaError(issue)) } } - /** * Decodes a typed input (the schema's `Encoded` type) against a schema, * returning a `Result` that succeeds with the decoded value or fails with a @@ -1810,7 +1694,6 @@ export const decodeResult: >( options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Result_.Result = decodeUnknownResult - /** * Decodes an `unknown` input against a schema, returning a `Promise` that * resolves with the decoded value or rejects with a {@link SchemaError} for @@ -1847,7 +1730,6 @@ export function decodeUnknownPromise>( return runSchemaErrorPromise(parser(input, options)) } } - /** * Decodes a typed input (the schema's `Encoded` type) against a schema, * returning a `Promise` that resolves with the decoded value or rejects with a @@ -1879,7 +1761,6 @@ export const decodePromise: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Promise = decodeUnknownPromise - /** * Decodes an `unknown` input against a schema synchronously, returning the * decoded value or throwing a {@link SchemaError} for schema mismatches. @@ -1923,7 +1804,6 @@ export function decodeUnknownSync>(schema: return runSchemaErrorSync(parser(input, options)) } } - /** * Decodes a typed input (the schema's `Encoded` type) against a schema * synchronously, returning the decoded value or throwing a {@link SchemaError} @@ -1954,7 +1834,6 @@ export const decodeSync: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => S["Type"] = decodeUnknownSync - /** * Encodes an `unknown` input against a schema, returning an `Effect` that * succeeds with the encoded value or fails with a {@link SchemaError}. @@ -1995,7 +1874,6 @@ export function encodeUnknownEffect(schema: S, options?: S return fromIssueEffect(parser(input, options)) } } - /** * Encodes a typed input (the schema's `Type`) against a schema, returning an * `Effect` that succeeds with the encoded value or fails with a @@ -2024,7 +1902,6 @@ export const encodeEffect: ( input: S["Type"], options?: SchemaAST.ParseOptions ) => Effect.Effect = encodeUnknownEffect - /** * Encodes an `unknown` input against a schema synchronously, returning an * `Exit` that is either a `Success` with the encoded value or a `Failure`. @@ -2060,7 +1937,6 @@ export function encodeUnknownExit>(schema: return fromIssueExit(parser(input, options)) } } - /** * Encodes a typed input (the schema's `Type`) against a schema synchronously, * returning an `Exit` that is either a `Success` with the encoded value or a @@ -2095,7 +1971,6 @@ export const encodeExit: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Exit_.Exit = encodeUnknownExit - /** * Encodes an `unknown` input against a schema, returning an `Option` that is * `Some` with the encoded value on success or `None` for schema mismatches. @@ -2127,7 +2002,6 @@ export const encodeUnknownOption: >( options?: SchemaAST.ParseOptions ) => (input: unknown, options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.encodeUnknownOption - /** * Encodes a typed input (the schema's `Type`) against a schema, returning an * `Option` that is `Some` with the encoded value on success or `None` for schema @@ -2157,7 +2031,6 @@ export const encodeOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.encodeOption - /** * Encodes an `unknown` input against a schema, returning a `Result` that * succeeds with the encoded value or fails with a {@link SchemaError} for schema @@ -2192,7 +2065,6 @@ export function encodeUnknownResult>(schema return Result_.mapError(parser(input, options), (issue) => new SchemaError(issue)) } } - /** * Encodes a typed input (the schema's `Type`) against a schema, returning a * `Result` that succeeds with the encoded value or fails with a @@ -2226,7 +2098,6 @@ export const encodeResult: >( options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Result_.Result = encodeUnknownResult - /** * Encodes an `unknown` input against a schema, returning a `Promise` that * resolves with the encoded value or rejects with a {@link SchemaError} for @@ -2262,7 +2133,6 @@ export function encodeUnknownPromise>( return runSchemaErrorPromise(parser(input, options)) } } - /** * Encodes a typed input (the schema's `Type`) against a schema, returning a * `Promise` that resolves with the encoded value or rejects with a @@ -2294,7 +2164,6 @@ export const encodePromise: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Promise = encodeUnknownPromise - /** * Encodes an `unknown` input against a schema synchronously, throwing a * {@link SchemaError} for schema mismatches. @@ -2327,7 +2196,6 @@ export function encodeUnknownSync>(schema: return runSchemaErrorSync(parser(input, options) as Effect.Effect) } } - /** * Encodes a typed input (the schema's `Type`) against a schema synchronously, * throwing a {@link SchemaError} for schema mismatches. @@ -2356,7 +2224,6 @@ export const encodeSync: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => S["Encoded"] = encodeUnknownSync - /** * Creates a schema from an AST (Abstract Syntax Tree) node. * @@ -2374,8 +2241,7 @@ export const encodeSync: >( * @category constructors * @since 3.10.0 */ -export const make: (ast: S["ast"], options?: object) => S = InternalSchema.make - +export const make: (ast: S["ast"], options?: object) => S = InternalMake.make /** * Checks whether a value is a `Schema`. * @@ -2385,7 +2251,6 @@ export const make: (ast: S["ast"], options?: object) => S export function isSchema(u: unknown): u is Top { return Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId } - /** * Type-level representation returned by {@link optionalKey}. * @@ -2413,12 +2278,10 @@ export interface optionalKey extends readonly "Iso": S["Iso"] readonly schema: S } - interface optionalKeyLambda extends Lambda { (self: S): optionalKey readonly "~lambda.out": this["~lambda.in"] extends Constraint ? optionalKey : never } - /** * Creates an exact optional key schema for struct fields. Unlike `optional`, * this creates exact optional properties (not `| undefined`) that can be @@ -2441,16 +2304,14 @@ interface optionalKeyLambda extends Lambda { * @category combinators * @since 4.0.0 */ -export const optionalKey = Struct_.lambda((schema) => +export const optionalKey: optionalKeyLambda = Struct_.lambda((schema) => make(SchemaAST.optionalKey(schema.ast), { schema }) ) - interface requiredKeyLambda extends Lambda { (self: optionalKey): S readonly "~lambda.out": this["~lambda.in"] extends optionalKey ? this["~lambda.in"]["schema"] : "Error: schema not eligible for requiredKey" } - /** * Reverses `optionalKey` and returns the inner required schema. * @@ -2462,8 +2323,7 @@ interface requiredKeyLambda extends Lambda { * @category combinators * @since 4.0.0 */ -export const requiredKey = Struct_.lambda((self) => self.schema) - +export const requiredKey: requiredKeyLambda = Struct_.lambda((self) => self.schema) /** * Type-level representation returned by {@link optional}. * @@ -2473,12 +2333,10 @@ export const requiredKey = Struct_.lambda((self) => self.sche export interface optional extends optionalKey> { readonly "Rebuild": optional } - interface optionalLambda extends Lambda { (self: S): optional readonly "~lambda.out": this["~lambda.in"] extends Constraint ? optional : never } - /** * Marks a struct field as optional, allowing the key to be absent or * `undefined`. @@ -2508,17 +2366,15 @@ interface optionalLambda extends Lambda { * @category combinators * @since 3.10.0 */ -export const optional = Struct_.lambda((self) => { +export const optional: optionalLambda = Struct_.lambda((self) => { const schema = UndefinedOr(self) return make(SchemaAST.optional(self.ast), { schema }) }) - interface requiredLambda extends Lambda { (self: optional): S readonly "~lambda.out": this["~lambda.in"] extends optional ? this["~lambda.in"]["schema"]["members"][0] : "Error: schema not eligible for required" } - /** * Reverses `optional` and returns the inner schema. * @@ -2534,8 +2390,7 @@ interface requiredLambda extends Lambda { * @category combinators * @since 3.10.0 */ -export const required = Struct_.lambda((self) => self.schema.members[0]) - +export const required: requiredLambda = Struct_.lambda((self) => self.schema.members[0]) /** * Type-level representation returned by {@link mutableKey}. * @@ -2563,12 +2418,10 @@ export interface mutableKey extends readonly "Iso": S["Iso"] readonly schema: S } - interface mutableKeyLambda extends Lambda { (self: S): mutableKey readonly "~lambda.out": this["~lambda.in"] extends Constraint ? mutableKey : never } - /** * Makes a struct field mutable (removes the `readonly` modifier on the property). * Use {@link readonlyKey} to reverse. @@ -2576,16 +2429,14 @@ interface mutableKeyLambda extends Lambda { * @category combinators * @since 4.0.0 */ -export const mutableKey = Struct_.lambda((schema) => +export const mutableKey: mutableKeyLambda = Struct_.lambda((schema) => make(SchemaAST.mutableKey(schema.ast), { schema }) ) - interface readonlyKeyLambda extends Lambda { (self: mutableKey): S readonly "~lambda.out": this["~lambda.in"] extends mutableKey ? this["~lambda.in"]["schema"] : "Error: schema not eligible for readonlyKey" } - /** * Reverses `mutableKey` and returns the inner readonly schema. * @@ -2597,8 +2448,7 @@ interface readonlyKeyLambda extends Lambda { * @category combinators * @since 4.0.0 */ -export const readonlyKey = Struct_.lambda((self) => self.schema) - +export const readonlyKey: readonlyKeyLambda = Struct_.lambda((self) => self.schema) /** * Type-level representation returned by {@link toType}. * @@ -2626,12 +2476,10 @@ export interface toType extends readonly "Iso": S["Iso"] readonly schema: S } - interface toTypeLambda extends Lambda { (self: S): toType readonly "~lambda.out": this["~lambda.in"] extends Constraint ? toType : never } - /** * Extracts the type-side schema: sets `Encoded` to equal the decoded `Type`, * discarding the encoding transformation path. @@ -2639,8 +2487,9 @@ interface toTypeLambda extends Lambda { * @category transforming * @since 4.0.0 */ -export const toType = Struct_.lambda((schema) => make(SchemaAST.toType(schema.ast), { schema })) - +export const toType: toTypeLambda = Struct_.lambda((schema) => + make(SchemaAST.toType(schema.ast), { schema }) +) /** * Type-level representation returned by {@link toEncoded}. * @@ -2668,12 +2517,10 @@ export interface toEncoded extends readonly "Iso": S["Encoded"] readonly schema: S } - interface toEncodedLambda extends Lambda { (self: S): toEncoded readonly "~lambda.out": this["~lambda.in"] extends Constraint ? toEncoded : never } - /** * Extracts the encoded-side schema: sets `Type` to equal the `Encoded`, * discarding the decoding transformation path. @@ -2681,10 +2528,10 @@ interface toEncodedLambda extends Lambda { * @category transforming * @since 4.0.0 */ -export const toEncoded = Struct_.lambda((schema) => make(SchemaAST.toEncoded(schema.ast), { schema })) - +export const toEncoded: toEncodedLambda = Struct_.lambda((schema) => + make(SchemaAST.toEncoded(schema.ast), { schema }) +) const FlipTypeId = "~effect/Schema/flip" - /** * Type-level representation returned by {@link flip}. * @@ -2717,7 +2564,6 @@ export interface flip extends function isFlip$(schema: Top): schema is flip { return Predicate.hasProperty(schema, FlipTypeId) && schema[FlipTypeId] === FlipTypeId } - /** * Swaps the decoded and encoded sides of a schema. * @@ -2749,7 +2595,6 @@ export function flip(schema: S): flip { } return make(SchemaAST.flip(schema.ast), { [FlipTypeId]: FlipTypeId, schema }) } - /** * Type-level representation returned by {@link Literal}. * @@ -2762,7 +2607,6 @@ export interface Literal readonly literal: L transform(to: L2): decodeTo, Literal> } - /** * Creates a schema for a single literal value (string, number, bigint, boolean, or null). * @@ -2794,7 +2638,6 @@ export function Literal(literal: L): Literal - - type AppendType< - Template extends string, - Next - > = Next extends LiteralPart ? `${Template}${Next}` - : Next extends { readonly Encoded: infer E extends LiteralPart } ? `${Template}${E}` - : never - + type Parts = ReadonlyArray + type AppendType